Grid layout (#570)

This PR adds a basic grid layout to Masonry and Xilem.

The way this layout works is it has a fixed grid based on the initial
size passed in, and grid items are placed based on the position
requested. Grid items are allowed to span more than one cell, if
requested.

There are potential improvements that could be done, like the use of
intrinsic sizing for varied column width based on content size. Though
that could be done in the future taffy layout if we want to keep this
one simple.


~~This PR is still a draft because of one remaining concern. I was not
able to successfully optimize with conditional calls to child widgets
for layout. It led to crashes about the paint rects not being within the
widget's paint rect. `Error in 'Grid' #16: paint_rect Rect { x0: 0.0,
y0: 0.0, x1: 800.0, y1: 610.0 } doesn't contain paint_rect Rect { x0:
400.5, y0: 0.0, x1: 800.5, y1: 150.0 } of child widget 'Button' #5`. My
failed attempt at fixing it is commented out.~~

Since I am rusty on View Sequences, a lot of that code is based on the
Flex implementation. Let me know if I did anything incorrectly or if any
of it is unnecessary, or if anything is missing.

---------

Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
This commit is contained in:
Jared O'Connell 2024-09-11 10:55:19 -04:00 committed by GitHub
parent 9a3c8e308c
commit 3726e91a48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1061 additions and 59 deletions

View File

@ -0,0 +1,121 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
//! Shows how to use a grid layout in Masonry.
// On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"]
use masonry::app_driver::{AppDriver, DriverCtx};
use masonry::dpi::LogicalSize;
use masonry::widget::{Button, Grid, GridParams, Prose, RootWidget, SizedBox};
use masonry::{Action, Color, PointerButton, WidgetId};
use parley::layout::Alignment;
use winit::window::Window;
struct Driver {
grid_spacing: f64,
}
impl AppDriver for Driver {
fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) {
if let Action::ButtonPressed(button) = action {
if button == PointerButton::Primary {
self.grid_spacing += 1.0;
} else if button == PointerButton::Secondary {
self.grid_spacing -= 1.0;
} else {
self.grid_spacing += 0.5;
}
ctx.get_root::<RootWidget<Grid>>()
.get_element()
.set_spacing(self.grid_spacing);
}
}
}
fn grid_button(params: GridParams) -> Button {
Button::new(format!(
"X: {}, Y: {}, W: {}, H: {}",
params.x, params.y, params.width, params.height
))
}
pub fn main() {
let label = SizedBox::new(
Prose::new("Change spacing by right and left clicking on the buttons")
.with_text_size(14.0)
.with_text_alignment(Alignment::Middle),
)
.border(Color::rgb8(40, 40, 80), 1.0);
let button_inputs = vec![
GridParams {
x: 0,
y: 0,
width: 1,
height: 1,
},
GridParams {
x: 2,
y: 0,
width: 2,
height: 1,
},
GridParams {
x: 0,
y: 1,
width: 1,
height: 2,
},
GridParams {
x: 1,
y: 1,
width: 2,
height: 2,
},
GridParams {
x: 3,
y: 1,
width: 1,
height: 1,
},
GridParams {
x: 3,
y: 2,
width: 1,
height: 1,
},
GridParams {
x: 0,
y: 3,
width: 4,
height: 1,
},
];
let driver = Driver { grid_spacing: 1.0 };
// Arrange widgets in a 4 by 4 grid.
let mut main_widget = Grid::with_dimensions(4, 4)
.with_spacing(driver.grid_spacing)
.with_child(label, GridParams::new(1, 0, 1, 1));
for button_input in button_inputs {
let button = grid_button(button_input);
main_widget = main_widget.with_child(button, button_input);
}
let window_size = LogicalSize::new(800.0, 500.0);
let window_attributes = Window::default_attributes()
.with_title("Grid Layout")
.with_resizable(true)
.with_min_inner_size(window_size);
masonry::event_loop_runner::run(
masonry::event_loop_runner::EventLoop::with_user_event(),
window_attributes,
RootWidget::new(main_widget),
driver,
)
.unwrap();
}

View File

@ -524,7 +524,7 @@ impl<'a> WidgetMut<'a, Flex> {
///
/// # Panics
///
/// Panics if the the element at `idx` is not a widget.
/// Panics if the element at `idx` is not a widget.
pub fn update_child_flex_params(&mut self, idx: usize, params: impl Into<FlexParams>) {
let child = &mut self.widget.children[idx];
let child_val = std::mem::replace(child, Child::FixedSpacer(0.0, 0.0));

426
masonry/src/widget/grid.rs Normal file
View File

@ -0,0 +1,426 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use accesskit::Role;
use smallvec::SmallVec;
use tracing::{trace_span, Span};
use vello::kurbo::{Affine, Line, Stroke};
use vello::Scene;
use crate::theme::get_debug_color;
use crate::widget::WidgetMut;
use crate::{
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod,
};
pub struct Grid {
children: Vec<Child>,
grid_width: i32,
grid_height: i32,
grid_spacing: f64,
}
struct Child {
widget: WidgetPod<Box<dyn Widget>>,
x: i32,
y: i32,
width: i32,
height: i32,
}
#[derive(Default, Debug, Copy, Clone, PartialEq)]
pub struct GridParams {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
// --- MARK: IMPL GRID ---
impl Grid {
pub fn with_dimensions(width: i32, height: i32) -> Self {
Grid {
children: Vec::new(),
grid_width: width,
grid_height: height,
grid_spacing: 0.0,
}
}
pub fn with_spacing(mut self, spacing: f64) -> Self {
self.grid_spacing = spacing;
self
}
/// Builder-style variant of [`WidgetMut::add_child`].
///
/// Convenient for assembling a group of widgets in a single expression.
pub fn with_child(self, child: impl Widget, params: GridParams) -> Self {
self.with_child_pod(WidgetPod::new(Box::new(child)), params)
}
pub fn with_child_id(self, child: impl Widget, id: WidgetId, params: GridParams) -> Self {
self.with_child_pod(WidgetPod::new_with_id(Box::new(child), id), params)
}
pub fn with_child_pod(
mut self,
widget: WidgetPod<Box<dyn Widget>>,
params: GridParams,
) -> Self {
let child = Child {
widget,
x: params.x,
y: params.y,
width: params.width,
height: params.height,
};
self.children.push(child);
self
}
}
// --- MARK: IMPL CHILD ---
impl Child {
fn widget_mut(&mut self) -> Option<&mut WidgetPod<Box<dyn Widget>>> {
Some(&mut self.widget)
}
fn widget(&self) -> Option<&WidgetPod<Box<dyn Widget>>> {
Some(&self.widget)
}
fn update_params(&mut self, params: GridParams) {
self.x = params.x;
self.y = params.y;
self.width = params.width;
self.height = params.height;
}
}
fn new_grid_child(params: GridParams, widget: WidgetPod<Box<dyn Widget>>) -> Child {
Child {
widget,
x: params.x,
y: params.y,
width: params.width,
height: params.height,
}
}
// --- MARK: IMPL GRIDPARAMS ---
impl GridParams {
pub fn new(mut x: i32, mut y: i32, mut width: i32, mut height: i32) -> GridParams {
if x < 0 {
debug_panic!("Grid x value should be a non-negative number; got {}", x);
x = 0;
}
if y < 0 {
debug_panic!("Grid y value should be a non-negative number; got {}", y);
y = 0;
}
if width <= 0 {
debug_panic!(
"Grid width value should be a positive nonzero number; got {}",
width
);
width = 1;
}
if height <= 0 {
debug_panic!(
"Grid height value should be a positive nonzero number; got {}",
height
);
height = 1;
}
GridParams {
x,
y,
width,
height,
}
}
}
// --- MARK: WIDGETMUT---
impl<'a> WidgetMut<'a, Grid> {
/// Add a child widget.
///
/// See also [`with_child`].
///
/// [`with_child`]: Grid::with_child
pub fn add_child(&mut self, child: impl Widget, params: GridParams) {
let child_pod: WidgetPod<Box<dyn Widget>> = WidgetPod::new(Box::new(child));
self.insert_child_pod(child_pod, params);
}
pub fn add_child_id(&mut self, child: impl Widget, id: WidgetId, params: GridParams) {
let child_pod: WidgetPod<Box<dyn Widget>> = WidgetPod::new_with_id(Box::new(child), id);
self.insert_child_pod(child_pod, params);
}
/// Add a child widget.
pub fn insert_child_pod(&mut self, widget: WidgetPod<Box<dyn Widget>>, params: GridParams) {
let child = new_grid_child(params, widget);
self.widget.children.push(child);
self.ctx.children_changed();
self.ctx.request_layout();
}
pub fn insert_grid_child_at(
&mut self,
idx: usize,
child: impl Widget,
params: impl Into<GridParams>,
) {
self.insert_grid_child_pod(idx, WidgetPod::new(Box::new(child)), params);
}
pub fn insert_grid_child_pod(
&mut self,
idx: usize,
child: WidgetPod<Box<dyn Widget>>,
params: impl Into<GridParams>,
) {
let child = new_grid_child(params.into(), child);
self.widget.children.insert(idx, child);
self.ctx.children_changed();
self.ctx.request_layout();
}
pub fn set_spacing(&mut self, spacing: f64) {
self.widget.grid_spacing = spacing;
self.ctx.request_layout();
}
pub fn set_width(&mut self, width: i32) {
self.widget.grid_width = width;
self.ctx.request_layout();
}
pub fn set_height(&mut self, height: i32) {
self.widget.grid_height = height;
self.ctx.request_layout();
}
pub fn child_mut(&mut self, idx: usize) -> Option<WidgetMut<'_, Box<dyn Widget>>> {
let child = match self.widget.children[idx].widget_mut() {
Some(widget) => widget,
None => return None,
};
Some(self.ctx.get_mut(child))
}
/// Updates the grid parameters for the child at `idx`,
///
/// # Panics
///
/// Panics if the element at `idx` is not a widget.
pub fn update_child_grid_params(&mut self, idx: usize, params: GridParams) {
let child = &mut self.widget.children[idx];
child.update_params(params);
self.ctx.request_layout();
}
pub fn remove_child(&mut self, idx: usize) {
let child = self.widget.children.remove(idx);
self.ctx.remove_child(child.widget);
self.ctx.request_layout();
}
}
// --- MARK: IMPL WIDGET---
impl Widget for Grid {
fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {}
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) {
child.lifecycle(ctx, event);
}
}
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.debug_check("Grid");
let total_size = bc.max();
let width_unit = (total_size.width + self.grid_spacing) / (self.grid_width as f64);
let height_unit = (total_size.height + self.grid_spacing) / (self.grid_height as f64);
for child in &mut self.children {
let cell_size = Size::new(
child.width as f64 * width_unit - self.grid_spacing,
child.height as f64 * height_unit - self.grid_spacing,
);
let child_bc = BoxConstraints::new(cell_size, cell_size);
let _ = ctx.run_layout(&mut child.widget, &child_bc);
ctx.place_child(
&mut child.widget,
Point::new(child.x as f64 * width_unit, child.y as f64 * height_unit),
);
}
total_size
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
// paint the baseline if we're debugging layout
if ctx.debug_paint && ctx.widget_state.baseline_offset != 0.0 {
let color = get_debug_color(ctx.widget_id().to_raw());
let my_baseline = ctx.size().height - ctx.widget_state.baseline_offset;
let line = Line::new((0.0, my_baseline), (ctx.size().width, my_baseline));
let stroke_style = Stroke::new(1.0).with_dashes(0., [4.0, 4.0]);
scene.stroke(&stroke_style, Affine::IDENTITY, color, None, &line);
}
}
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(&mut self, _: &mut AccessCtx) {}
fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
self.children
.iter()
.filter_map(|child| child.widget())
.map(|widget_pod| widget_pod.id())
.collect()
}
fn make_trace_span(&self) -> Span {
trace_span!("Grid")
}
}
// --- MARK: TESTS ---
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_render_snapshot;
use crate::testing::TestHarness;
use crate::widget::button;
#[test]
fn test_grid_basics() {
// Start with a 1x1 grid
let widget = Grid::with_dimensions(1, 1)
.with_child(button::Button::new("A"), GridParams::new(0, 0, 1, 1));
let mut harness = TestHarness::create(widget);
// Snapshot with the single widget.
assert_render_snapshot!(harness, "initial_1x1");
// Expand it to a 4x4 grid
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.set_width(4);
});
assert_render_snapshot!(harness, "expanded_4x1");
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.set_height(4);
});
assert_render_snapshot!(harness, "expanded_4x4");
// Add a widget that takes up more than one horizontal cell
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.add_child(button::Button::new("B"), GridParams::new(1, 0, 3, 1));
});
assert_render_snapshot!(harness, "with_horizontal_widget");
// Add a widget that takes up more than one vertical cell
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.add_child(button::Button::new("C"), GridParams::new(0, 1, 1, 3));
});
assert_render_snapshot!(harness, "with_vertical_widget");
// Add a widget that takes up more than one horizontal and vertical cell
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.add_child(button::Button::new("D"), GridParams::new(1, 1, 2, 2));
});
assert_render_snapshot!(harness, "with_2x2_widget");
// Change the spacing
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.set_spacing(7.0);
});
assert_render_snapshot!(harness, "with_changed_spacing");
// Make the spacing negative
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.set_spacing(-4.0);
});
assert_render_snapshot!(harness, "with_negative_spacing");
}
#[test]
fn test_widget_removal_and_modification() {
let widget = Grid::with_dimensions(2, 2)
.with_child(button::Button::new("A"), GridParams::new(0, 0, 1, 1));
let mut harness = TestHarness::create(widget);
// Snapshot with the single widget.
assert_render_snapshot!(harness, "initial_2x2");
// Now remove the widget
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.remove_child(0);
});
assert_render_snapshot!(harness, "2x2_with_removed_widget");
// Add it back
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.add_child(button::Button::new("A"), GridParams::new(0, 0, 1, 1));
});
assert_render_snapshot!(harness, "initial_2x2"); // Should be back to the original state
// Change the grid params to position it on the other corner
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.update_child_grid_params(0, GridParams::new(1, 1, 1, 1));
});
assert_render_snapshot!(harness, "moved_2x2_1");
// Now make it take up the entire grid
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.update_child_grid_params(0, GridParams::new(0, 0, 2, 2));
});
assert_render_snapshot!(harness, "moved_2x2_2");
}
#[test]
fn test_widget_order() {
let widget = Grid::with_dimensions(2, 2)
.with_child(button::Button::new("A"), GridParams::new(0, 0, 1, 1));
let mut harness = TestHarness::create(widget);
// Snapshot with the single widget.
assert_render_snapshot!(harness, "initial_2x2");
// Order sets the draw order, so draw a widget over A by adding it after
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.add_child(button::Button::new("B"), GridParams::new(0, 0, 1, 1));
});
assert_render_snapshot!(harness, "2x2_with_overlapping_b");
// Draw a widget under the others by putting it at index 0
// Make it wide enough to see it stick out, with half of it under A and B.
harness.edit_root_widget(|mut grid| {
let mut grid = grid.downcast::<Grid>();
grid.insert_grid_child_at(0, button::Button::new("C"), GridParams::new(0, 0, 2, 1));
});
assert_render_snapshot!(harness, "2x2_with_overlapping_c");
}
}

View File

@ -17,6 +17,7 @@ mod align;
mod button;
mod checkbox;
mod flex;
mod grid;
mod image;
mod label;
mod portal;
@ -36,6 +37,7 @@ pub use align::Align;
pub use button::Button;
pub use checkbox::Checkbox;
pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment};
pub use grid::{Grid, GridParams};
pub use label::{Label, LineBreaking};
pub use portal::Portal;
pub use progress_bar::ProgressBar;

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4aef605773c92b01166520d3ca8065116d46540bc3880978b5f248c95c61fd8d
size 5001

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ec7a39d12493409ed242cc1f41e3d4dce46a96c481d2cdc38d4da185269465e0
size 5321

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c1515c193d2b494e792f05b5d088d1566774285e0df448ea9bf364ce03ab712
size 4472

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f7ce584ad5a911bb7bb8c35f3066ac3031c7ade64692896f7586e20c391b624
size 5052

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58796a0ff44c059d90b9a2e424d390721b5e0052eccbd3b6f003d1739d771440
size 5186

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f92e5d63eab954b39012854bafa8ce2d9badfdae4304108bd7e290c2b1cb7143
size 5026

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:33aabab29c8651a7efaf88d7609cb50b93e8864367ddeff54a77dc435bb746b0
size 5312

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:47b741e1bcdc9ad915c2f17f3fededa3435bc089a3fea2309235c913cfce331a
size 5321

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f92e5d63eab954b39012854bafa8ce2d9badfdae4304108bd7e290c2b1cb7143
size 5026

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e52f1f8d696af90e035cbb77f1eda99d14bf6fc2d800866040d0976a7c5d2a25
size 6693

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81afa7e7ef34369d9edfe042b0c60d1eef1b911c098ac625164171b8b7e49e11
size 6971

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3602efdabe62bdb4592090afcb80256bfe9ba339d5b87f1271f36cbca7c35e0a
size 5609

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1d1c79d02e5f0b22b6326e7926c0af2880cdb9c4e852b04fde63cab9cce10da5
size 6897

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:871275dca581cd2201a4d88e7bf50a2497f31f87afc953446d1f80293703deee
size 6173

View File

@ -1,14 +1,14 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use masonry::widget::{CrossAxisAlignment, MainAxisAlignment};
use masonry::widget::{CrossAxisAlignment, GridParams, MainAxisAlignment};
use winit::dpi::LogicalSize;
use winit::error::EventLoopError;
use winit::window::Window;
use xilem::view::{Flex, FlexSequence};
use xilem::view::{grid, Flex, FlexSequence, FlexSpacer, GridExt, GridSequence};
use xilem::EventLoopBuilder;
use xilem::{
view::{button, flex, label, sized_box, Axis, FlexExt as _, FlexSpacer},
view::{button, flex, label, sized_box, Axis},
EventLoop, WidgetView, Xilem,
};
@ -184,55 +184,54 @@ impl Calculator {
}
}
fn num_row(nums: [&'static str; 3], row: i32) -> impl GridSequence<Calculator> {
let mut views: Vec<_> = vec![];
for (i, num) in nums.iter().enumerate() {
views.push(digit_button(num).grid_pos(i as i32, row));
}
views
}
const DISPLAY_FONT_SIZE: f32 = 30.;
const GRID_GAP: f64 = 2.;
fn app_logic(data: &mut Calculator) -> impl WidgetView<Calculator> {
let num_row = |nums: [&'static str; 3], operator| {
flex_row((
nums.map(|num| digit_button(num).flex(1.)),
operator_button(operator).flex(1.),
))
};
flex((
// Display
centered_flex_row((
FlexSpacer::Flex(0.1),
display_label(data.numbers[0].as_ref()),
data.operation
.map(|operation| display_label(operation.as_str())),
display_label(data.numbers[1].as_ref()),
data.result.is_some().then(|| display_label("=")),
data.result
.as_ref()
.map(|result| display_label(result.as_ref())),
FlexSpacer::Flex(0.1),
))
.flex(1.0),
FlexSpacer::Fixed(10.0),
// Top row
flex_row((
expanded_button("CE", Calculator::clear_entry).flex(1.),
expanded_button("C", Calculator::clear_all).flex(1.),
expanded_button("DEL", Calculator::on_delete).flex(1.),
operator_button(MathOperator::Divide).flex(1.),
))
.flex(1.0),
num_row(["7", "8", "9"], MathOperator::Multiply).flex(1.0),
num_row(["4", "5", "6"], MathOperator::Subtract).flex(1.0),
num_row(["1", "2", "3"], MathOperator::Add).flex(1.0),
// bottom row
flex_row((
expanded_button("±", Calculator::negate).flex(1.),
digit_button("0").flex(1.),
digit_button(".").flex(1.),
expanded_button("=", Calculator::on_equals).flex(1.),
))
.flex(1.0),
))
.gap(GRID_GAP)
.cross_axis_alignment(CrossAxisAlignment::Fill)
.main_axis_alignment(MainAxisAlignment::End)
.must_fill_major_axis(true)
grid(
(
// Display
centered_flex_row((
FlexSpacer::Flex(0.1),
display_label(data.numbers[0].as_ref()),
data.operation
.map(|operation| display_label(operation.as_str())),
display_label(data.numbers[1].as_ref()),
data.result.is_some().then(|| display_label("=")),
data.result
.as_ref()
.map(|result| display_label(result.as_ref())),
FlexSpacer::Flex(0.1),
))
.grid_item(GridParams::new(0, 0, 4, 1)),
// Top row
expanded_button("CE", Calculator::clear_entry).grid_pos(0, 1),
expanded_button("C", Calculator::clear_all).grid_pos(1, 1),
expanded_button("DEL", Calculator::on_delete).grid_pos(2, 1),
operator_button(MathOperator::Divide).grid_pos(3, 1),
num_row(["7", "8", "9"], 2),
operator_button(MathOperator::Multiply).grid_pos(3, 2),
num_row(["4", "5", "6"], 3),
operator_button(MathOperator::Subtract).grid_pos(3, 3),
num_row(["1", "2", "3"], 4),
operator_button(MathOperator::Add).grid_pos(3, 4),
// bottom row
expanded_button("±", Calculator::negate).grid_pos(0, 5),
digit_button("0").grid_pos(1, 5),
digit_button(".").grid_pos(2, 5),
expanded_button("=", Calculator::on_equals).grid_pos(3, 5),
),
4,
6,
)
.spacing(GRID_GAP)
}
/// Creates a horizontal centered flex row designed for the display portion of the calculator.
@ -244,15 +243,6 @@ pub fn centered_flex_row<State, Seq: FlexSequence<State>>(sequence: Seq) -> Flex
.gap(5.)
}
/// Creates a horizontal filled flex row designed to be used in a grid.
pub fn flex_row<State, Seq: FlexSequence<State>>(sequence: Seq) -> Flex<Seq, State> {
flex(sequence)
.direction(Axis::Horizontal)
.cross_axis_alignment(CrossAxisAlignment::Fill)
.main_axis_alignment(MainAxisAlignment::SpaceEvenly)
.gap(GRID_GAP)
}
/// Returns a label intended to be used in the calculator's top display.
/// The default text size is out of proportion for this use case.
fn display_label(text: &str) -> impl WidgetView<Calculator> {

418
xilem/src/view/grid.rs Normal file
View File

@ -0,0 +1,418 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use std::marker::PhantomData;
use masonry::widget::GridParams;
use masonry::{
widget::{self, WidgetMut},
Widget,
};
use xilem_core::{
AppendVec, DynMessage, ElementSplice, MessageResult, Mut, SuperElement, View, ViewElement,
ViewMarker, ViewSequence,
};
use crate::{Pod, ViewCtx, WidgetView};
pub fn grid<State, Action, Seq: GridSequence<State, Action>>(
sequence: Seq,
width: i32,
height: i32,
) -> Grid<Seq, State, Action> {
Grid {
sequence,
spacing: 0.0,
phantom: PhantomData,
height,
width,
}
}
pub struct Grid<Seq, State, Action = ()> {
sequence: Seq,
spacing: f64,
width: i32,
height: i32,
/// Used to associate the State and Action in the call to `.grid()` with the State and Action
/// used in the View implementation, to allow inference to flow backwards, allowing State and
/// Action to be inferred properly
phantom: PhantomData<fn() -> (State, Action)>,
}
impl<Seq, State, Action> Grid<Seq, State, Action> {
#[track_caller]
pub fn spacing(mut self, spacing: f64) -> Self {
if spacing.is_finite() && spacing >= 0.0 {
self.spacing = spacing;
} else {
panic!("Invalid `spacing` {spacing}; expected a non-negative finite value.")
}
self
}
}
impl<Seq, State, Action> ViewMarker for Grid<Seq, State, Action> {}
impl<State, Action, Seq> View<State, Action, ViewCtx> for Grid<Seq, State, Action>
where
State: 'static,
Action: 'static,
Seq: GridSequence<State, Action>,
{
type Element = Pod<widget::Grid>;
type ViewState = Seq::SeqState;
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
let mut elements = AppendVec::default();
let mut widget = widget::Grid::with_dimensions(self.width, self.height);
widget = widget.with_spacing(self.spacing);
let seq_state = self.sequence.seq_build(ctx, &mut elements);
for child in elements.into_inner() {
widget = match child {
GridElement::Child(child, params) => widget.with_child_pod(child.inner, params),
}
}
(Pod::new(widget), seq_state)
}
fn rebuild<'el>(
&self,
prev: &Self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
mut element: Mut<'el, Self::Element>,
) -> Mut<'el, Self::Element> {
if prev.height != self.height {
element.set_height(self.height);
ctx.mark_changed();
}
if prev.width != self.width {
element.set_width(self.width);
ctx.mark_changed();
}
if prev.spacing != self.spacing {
element.set_spacing(self.spacing);
ctx.mark_changed();
}
let mut splice = GridSplice::new(element);
self.sequence
.seq_rebuild(&prev.sequence, view_state, ctx, &mut splice);
debug_assert!(splice.scratch.is_empty());
splice.element
}
fn teardown(
&self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
element: Mut<'_, Self::Element>,
) {
let mut splice = GridSplice::new(element);
self.sequence.seq_teardown(view_state, ctx, &mut splice);
debug_assert!(splice.scratch.into_inner().is_empty());
}
fn message(
&self,
view_state: &mut Self::ViewState,
id_path: &[xilem_core::ViewId],
message: DynMessage,
app_state: &mut State,
) -> MessageResult<Action> {
self.sequence
.seq_message(view_state, id_path, message, app_state)
}
}
// Used to become a reference form for editing. It's provided to rebuild and teardown.
impl ViewElement for GridElement {
type Mut<'w> = GridElementMut<'w>;
}
// Used to allow the item to be used as a generic item in ViewSequence.
impl SuperElement<GridElement> for GridElement {
fn upcast(child: GridElement) -> Self {
child
}
fn with_downcast_val<R>(
mut this: Mut<'_, Self>,
f: impl FnOnce(Mut<'_, GridElement>) -> R,
) -> (Self::Mut<'_>, R) {
let r = {
let parent = this.parent.reborrow_mut();
let reborrow = GridElementMut {
idx: this.idx,
parent,
};
f(reborrow)
};
(this, r)
}
}
impl<W: Widget> SuperElement<Pod<W>> for GridElement {
fn upcast(child: Pod<W>) -> Self {
// Getting here means that the widget didn't use .grid_item or .grid_pos.
// This currently places the widget in the top left cell.
// There is not much else, beyond purposefully failing, that can be done here,
// because there isn't enough information to determine an appropriate spot
// for the widget.
GridElement::Child(child.inner.boxed().into(), GridParams::new(1, 1, 1, 1))
}
fn with_downcast_val<R>(
mut this: Mut<'_, Self>,
f: impl FnOnce(Mut<'_, Pod<W>>) -> R,
) -> (Mut<'_, Self>, R) {
let ret = {
let mut child = this
.parent
.child_mut(this.idx)
.expect("This is supposed to be a widget");
let downcast = child.downcast();
f(downcast)
};
(this, ret)
}
}
// Used for building and rebuilding the ViewSequence
impl ElementSplice<GridElement> for GridSplice<'_> {
fn with_scratch<R>(&mut self, f: impl FnOnce(&mut AppendVec<GridElement>) -> R) -> R {
let ret = f(&mut self.scratch);
for element in self.scratch.drain() {
match element {
GridElement::Child(child, params) => {
self.element
.insert_grid_child_pod(self.idx, child.inner, params);
}
};
self.idx += 1;
}
ret
}
fn insert(&mut self, element: GridElement) {
match element {
GridElement::Child(child, params) => {
self.element
.insert_grid_child_pod(self.idx, child.inner, params);
}
};
self.idx += 1;
}
fn mutate<R>(&mut self, f: impl FnOnce(Mut<'_, GridElement>) -> R) -> R {
let child = GridElementMut {
parent: self.element.reborrow_mut(),
idx: self.idx,
};
let ret = f(child);
self.idx += 1;
ret
}
fn skip(&mut self, n: usize) {
self.idx += n;
}
fn delete<R>(&mut self, f: impl FnOnce(Mut<'_, GridElement>) -> R) -> R {
let ret = {
let child = GridElementMut {
parent: self.element.reborrow_mut(),
idx: self.idx,
};
f(child)
};
self.element.remove_child(self.idx);
ret
}
}
/// `GridSequence` is what allows an input to the grid that contains all the grid elements.
pub trait GridSequence<State, Action = ()>:
ViewSequence<State, Action, ViewCtx, GridElement>
{
}
impl<Seq, State, Action> GridSequence<State, Action> for Seq where
Seq: ViewSequence<State, Action, ViewCtx, GridElement>
{
}
/// A trait which extends a [`WidgetView`] with methods to provide parameters for a grid item
pub trait GridExt<State, Action>: WidgetView<State, Action> {
/// Applies [`impl Into<GridParams>`](`GridParams`) to this view. This allows the view
/// to be placed as a child within a [`Grid`] [`View`].
///
/// # Examples
/// ```
/// use masonry::widget::GridParams;
/// use xilem::{view::{button, prose, grid, GridExt}};
/// # use xilem::{WidgetView};
///
/// # fn view<State: 'static>() -> impl WidgetView<State> {
/// grid((
/// button("click me", |_| ()).grid_item(GridParams::new(0, 0, 2, 1)),
/// prose("a prose").grid_item(GridParams::new(1, 1, 1, 1)),
/// ), 2, 2)
/// # }
/// ```
fn grid_item(self, params: impl Into<GridParams>) -> GridItem<Self, State, Action>
where
State: 'static,
Action: 'static,
Self: Sized,
{
grid_item(self, params)
}
/// Applies a [`impl Into<GridParams>`](`GridParams`) with the specified position to this view.
/// This allows the view to be placed as a child within a [`Grid`] [`View`].
/// For instances where a grid item is expected to take up multiple cell units,
/// use [`GridExt::grid_item`]
///
/// # Examples
/// ```
/// use masonry::widget::GridParams;
/// use xilem::{view::{button, prose, grid, GridExt}};
/// # use xilem::{WidgetView};
///
/// # fn view<State: 'static>() -> impl WidgetView<State> {
/// grid((
/// button("click me", |_| ()).grid_pos(0, 0),
/// prose("a prose").grid_pos(1, 1),
/// ), 2, 2)
/// # }
fn grid_pos(self, x: i32, y: i32) -> GridItem<Self, State, Action>
where
State: 'static,
Action: 'static,
Self: Sized,
{
grid_item(self, GridParams::new(x, y, 1, 1))
}
}
impl<State, Action, V: WidgetView<State, Action>> GridExt<State, Action> for V {}
pub enum GridElement {
Child(Pod<Box<dyn Widget>>, GridParams),
}
pub struct GridElementMut<'w> {
parent: WidgetMut<'w, widget::Grid>,
idx: usize,
}
// Used for manipulating the ViewSequence.
pub struct GridSplice<'w> {
idx: usize,
element: WidgetMut<'w, widget::Grid>,
scratch: AppendVec<GridElement>,
}
impl<'w> GridSplice<'w> {
fn new(element: WidgetMut<'w, widget::Grid>) -> Self {
Self {
idx: 0,
element,
scratch: AppendVec::default(),
}
}
}
/// A `WidgetView` that can be used within a [`Grid`] [`View`]
pub struct GridItem<V, State, Action> {
view: V,
params: GridParams,
phantom: PhantomData<fn() -> (State, Action)>,
}
pub fn grid_item<V, State, Action>(
view: V,
params: impl Into<GridParams>,
) -> GridItem<V, State, Action>
where
State: 'static,
Action: 'static,
V: WidgetView<State, Action>,
{
GridItem {
view,
params: params.into(),
phantom: PhantomData,
}
}
impl<V, State, Action> ViewMarker for GridItem<V, State, Action> {}
impl<State, Action, V> View<State, Action, ViewCtx> for GridItem<V, State, Action>
where
State: 'static,
Action: 'static,
V: WidgetView<State, Action>,
{
type Element = GridElement;
type ViewState = V::ViewState;
fn build(&self, cx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
let (pod, state) = self.view.build(cx);
(
GridElement::Child(pod.inner.boxed().into(), self.params),
state,
)
}
fn rebuild<'el>(
&self,
prev: &Self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
mut element: Mut<'el, Self::Element>,
) -> Mut<'el, Self::Element> {
{
if self.params != prev.params {
element
.parent
.update_child_grid_params(element.idx, self.params);
}
let mut child = element
.parent
.child_mut(element.idx)
.expect("GridWrapper always has a widget child");
self.view
.rebuild(&prev.view, view_state, ctx, child.downcast());
}
element
}
fn teardown(
&self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
mut element: Mut<'_, Self::Element>,
) {
let mut child = element
.parent
.child_mut(element.idx)
.expect("GridWrapper always has a widget child");
self.view.teardown(view_state, ctx, child.downcast());
}
fn message(
&self,
view_state: &mut Self::ViewState,
id_path: &[xilem_core::ViewId],
message: DynMessage,
app_state: &mut State,
) -> MessageResult<Action> {
self.view.message(view_state, id_path, message, app_state)
}
}

View File

@ -16,6 +16,9 @@ pub use checkbox::*;
mod flex;
pub use flex::*;
mod grid;
pub use grid::*;
mod sized_box;
pub use sized_box::*;