Dynamic child for masonry `Button` (#1253)

Switching from `Button` to `Button<W>` so that it can contain other type
of widgets as well!

Edit: Now switching to `Button` with a `dyn Widget` as the child
instead!
This commit is contained in:
Nixon 2025-07-30 18:14:43 +08:00 committed by GitHub
parent 7054bd42a6
commit cc4f92812c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 114 additions and 91 deletions

View File

@ -4,6 +4,7 @@
//! A button widget.
use std::any::TypeId;
use std::sync::Arc;
use accesskit::{Node, Role};
use tracing::{Span, trace, trace_span};
@ -14,9 +15,9 @@ use vello::peniko::Color;
use crate::core::keyboard::{Key, NamedKey};
use crate::core::{
AccessCtx, AccessEvent, ArcStr, BoxConstraints, ChildrenIds, EventCtx, LayoutCtx, NewWidget,
PaintCtx, PointerEvent, PropertiesMut, PropertiesRef, RegisterCtx, TextEvent, Update,
UpdateCtx, Widget, WidgetId, WidgetMut, WidgetPod,
AccessCtx, AccessEvent, BoxConstraints, ChildrenIds, EventCtx, LayoutCtx, NewWidget, PaintCtx,
PointerEvent, PropertiesMut, PropertiesRef, RegisterCtx, TextEvent, Update, UpdateCtx, Widget,
WidgetId, WidgetMut, WidgetPod,
};
use crate::properties::{
ActiveBackground, Background, BorderColor, BorderWidth, BoxShadow, CornerRadius,
@ -24,61 +25,59 @@ use crate::properties::{
};
use crate::theme;
use crate::util::{fill, include_screenshot, stroke};
use crate::widgets::Label;
/// A button with a text label.
use super::Label;
/// A button with a child widget.
///
/// Emits [`ButtonPress`] when pressed.
///
#[doc = include_screenshot!("button_hello.png", "Button with text label.")]
pub struct Button {
label: WidgetPod<Label>,
child: WidgetPod<dyn Widget>,
}
// --- MARK: BUILDERS
impl Button {
/// Create a new button with a text label.
/// Create a new button with a child widget.
///
/// The child widget probably shouldn't be interactive,
/// to avoid behaviour which might be confusing to the user.
///
/// # Examples
///
/// ```
/// use masonry::widgets::{Button, Label};
/// use masonry::core::Widget;
///
/// let button = Button::new(Label::new("Increment").with_auto_id());
/// ```
pub fn new(child: NewWidget<impl Widget>) -> Self {
Self {
child: child.erased().to_pod(),
}
}
/// Create a new button with a label widget.
///
/// # Examples
///
/// ```
/// use masonry::widgets::Button;
/// use masonry::core::Widget;
///
/// let button = Button::new("Increment");
/// let button = Button::with_text("Increment");
/// ```
pub fn new(text: impl Into<ArcStr>) -> Self {
Self::from_label(Label::new(text).with_auto_id())
}
/// Create a new button with the provided [`Label`].
///
/// # Examples
///
/// ```
/// use masonry::core::{StyleProperty, Widget as _};
/// use masonry::peniko::Color;
/// use masonry::widgets::{Button, Label};
///
/// let label = Label::new("Increment").with_style(StyleProperty::FontSize(20.0));
/// let button = Button::from_label(label.with_auto_id());
/// ```
pub fn from_label(label: NewWidget<Label>) -> Self {
Self {
label: label.to_pod(),
}
pub fn with_text(text: impl Into<Arc<str>>) -> Self {
Self::new(Label::new(text).with_auto_id())
}
}
// --- MARK: WIDGETMUT
impl Button {
/// Set the text.
pub fn set_text(this: &mut WidgetMut<'_, Self>, new_text: impl Into<ArcStr>) {
Label::set_text(&mut Self::label_mut(this), new_text);
}
/// Get a mutable reference to the label.
pub fn label_mut<'t>(this: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, Label> {
this.ctx.get_mut(&mut this.widget.label)
pub fn child_mut<'t>(this: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, dyn Widget> {
this.ctx.get_mut(&mut this.widget.child)
}
}
@ -165,7 +164,7 @@ impl Widget for Button {
}
fn register_children(&mut self, ctx: &mut RegisterCtx<'_>) {
ctx.register_child(&mut self.label);
ctx.register_child(&mut self.child);
}
fn property_changed(&mut self, ctx: &mut UpdateCtx<'_>, property_type: TypeId) {
@ -196,8 +195,8 @@ impl Widget for Button {
let bc = border.layout_down(bc);
let bc = padding.layout_down(bc);
let label_size = ctx.run_layout(&mut self.label, &bc);
let baseline = ctx.child_baseline_offset(&self.label);
let label_size = ctx.run_layout(&mut self.child, &bc);
let baseline = ctx.child_baseline_offset(&self.child);
let size = label_size;
let (size, baseline) = padding.layout_up(size, baseline);
@ -212,7 +211,7 @@ impl Widget for Button {
// TODO - Figure out how to handle cases where label size doesn't fit bc.
let size = initial_bc.constrain(size);
let label_offset = (size.to_vec2() - label_size.to_vec2()) / 2.0;
ctx.place_child(&mut self.label, label_offset.to_point());
ctx.place_child(&mut self.child, label_offset.to_point());
// TODO - pos = (size - label_size) / 2
@ -278,7 +277,7 @@ impl Widget for Button {
}
fn children_ids(&self) -> ChildrenIds {
ChildrenIds::from_slice(&[self.label.id()])
ChildrenIds::from_slice(&[self.child.id()])
}
fn accepts_focus(&self) -> bool {
@ -307,12 +306,12 @@ mod tests {
use crate::properties::TextColor;
use crate::testing::{TestHarness, assert_render_snapshot, widget_ids};
use crate::theme::{ACCENT_COLOR, default_property_set};
use crate::widgets::{Grid, GridParams};
use crate::widgets::{Grid, GridParams, Label};
#[test]
fn simple_button() {
let [button_id] = widget_ids();
let widget = NewWidget::new_with_id(Button::new("Hello"), button_id);
let widget = NewWidget::new_with_id(Button::with_text("Hello"), button_id);
let window_size = Size::new(100.0, 40.0);
let mut harness =
@ -356,7 +355,7 @@ mod tests {
Properties::new().with(TextColor::new(ACCENT_COLOR)),
);
let button = NewWidget::new(Button::from_label(label));
let button = NewWidget::new(Button::new(label));
let mut harness = TestHarness::create_with_size(
default_property_set(),
@ -368,7 +367,7 @@ mod tests {
};
let image_2 = {
let button = NewWidget::new(Button::new("Hello world"));
let button = NewWidget::new(Button::with_text("Hello world"));
let mut harness = TestHarness::create_with_size(
default_property_set(),
@ -377,9 +376,11 @@ mod tests {
);
harness.edit_root_widget(|mut button| {
Button::set_text(&mut button, "The quick brown fox jumps over the lazy dog");
let mut label = Button::child_mut(&mut button);
let mut label = label.downcast();
Label::set_text(&mut label, "The quick brown fox jumps over the lazy dog");
let mut label = Button::label_mut(&mut button);
label.insert_prop(TextColor::new(ACCENT_COLOR));
Label::insert_style(&mut label, StyleProperty::FontSize(20.0));
});
@ -394,7 +395,7 @@ mod tests {
#[test]
fn set_properties() {
let red = crate::palette::css::RED;
let button = NewWidget::new(Button::new("Some random text"));
let button = NewWidget::new(Button::with_text("Some random text"));
let window_size = Size::new(200.0, 80.0);
let mut harness =
@ -406,7 +407,7 @@ mod tests {
button.insert_prop(CornerRadius { radius: 20.0 });
button.insert_prop(Padding::from_vh(3., 8.));
let mut label = Button::label_mut(&mut button);
let mut label = Button::child_mut(&mut button);
label.insert_prop(TextColor::new(red));
});
@ -419,10 +420,22 @@ mod tests {
let grid = Grid::with_dimensions(2, 2)
.with_spacing(40.0)
.with_child(Button::new("A").with_auto_id(), GridParams::new(0, 0, 1, 1))
.with_child(Button::new("B").with_auto_id(), GridParams::new(1, 0, 1, 1))
.with_child(Button::new("C").with_auto_id(), GridParams::new(0, 1, 1, 1))
.with_child(Button::new("D").with_auto_id(), GridParams::new(1, 1, 1, 1));
.with_child(
Button::with_text("A").with_auto_id(),
GridParams::new(0, 0, 1, 1),
)
.with_child(
Button::with_text("B").with_auto_id(),
GridParams::new(1, 0, 1, 1),
)
.with_child(
Button::with_text("C").with_auto_id(),
GridParams::new(0, 1, 1, 1),
)
.with_child(
Button::with_text("D").with_auto_id(),
GridParams::new(1, 1, 1, 1),
);
let root_widget =
NewWidget::new_with_props(grid, Properties::new().with(Padding::all(20.0)));

View File

@ -346,13 +346,13 @@ mod tests {
use super::*;
use crate::testing::{TestHarness, assert_render_snapshot};
use crate::theme::default_property_set;
use crate::widgets::button;
use crate::widgets::Button;
#[test]
fn test_grid_basics() {
// Start with a 1x1 grid
let widget = NewWidget::new(Grid::with_dimensions(1, 1).with_child(
button::Button::new("A").with_auto_id(),
Button::with_text("A").with_auto_id(),
GridParams::new(0, 0, 1, 1),
));
let window_size = Size::new(200.0, 200.0);
@ -376,7 +376,7 @@ mod tests {
harness.edit_root_widget(|mut grid| {
Grid::add_child(
&mut grid,
button::Button::new("B").with_auto_id(),
Button::with_text("B").with_auto_id(),
GridParams::new(1, 0, 3, 1),
);
});
@ -386,7 +386,7 @@ mod tests {
harness.edit_root_widget(|mut grid| {
Grid::add_child(
&mut grid,
button::Button::new("C").with_auto_id(),
Button::with_text("C").with_auto_id(),
GridParams::new(0, 1, 1, 3),
);
});
@ -396,7 +396,7 @@ mod tests {
harness.edit_root_widget(|mut grid| {
Grid::add_child(
&mut grid,
button::Button::new("D").with_auto_id(),
Button::with_text("D").with_auto_id(),
GridParams::new(1, 1, 2, 2),
);
});
@ -418,7 +418,7 @@ mod tests {
#[test]
fn test_widget_removal_and_modification() {
let widget = NewWidget::new(Grid::with_dimensions(2, 2).with_child(
button::Button::new("A").with_auto_id(),
Button::with_text("A").with_auto_id(),
GridParams::new(0, 0, 1, 1),
));
let window_size = Size::new(200.0, 200.0);
@ -437,7 +437,7 @@ mod tests {
harness.edit_root_widget(|mut grid| {
Grid::add_child(
&mut grid,
button::Button::new("A").with_auto_id(),
Button::with_text("A").with_auto_id(),
GridParams::new(0, 0, 1, 1),
);
});
@ -459,7 +459,7 @@ mod tests {
#[test]
fn test_widget_order() {
let widget = NewWidget::new(Grid::with_dimensions(2, 2).with_child(
button::Button::new("A").with_auto_id(),
Button::with_text("A").with_auto_id(),
GridParams::new(0, 0, 1, 1),
));
let window_size = Size::new(200.0, 200.0);
@ -472,7 +472,7 @@ mod tests {
harness.edit_root_widget(|mut grid| {
Grid::add_child(
&mut grid,
button::Button::new("B").with_auto_id(),
Button::with_text("B").with_auto_id(),
GridParams::new(0, 0, 1, 1),
);
});
@ -484,7 +484,7 @@ mod tests {
Grid::insert_grid_child_at(
&mut grid,
0,
button::Button::new("C").with_auto_id(),
Button::with_text("C").with_auto_id(),
GridParams::new(0, 0, 2, 1),
);
});

View File

@ -286,7 +286,7 @@ mod tests {
use super::*;
use crate::testing::{TestHarness, assert_render_snapshot};
use crate::theme::default_property_set;
use crate::widgets::button;
use crate::widgets::Button;
#[test]
fn test_indexed_stack_basics() {
@ -298,14 +298,14 @@ mod tests {
assert_render_snapshot!(harness, "indexed_stack_empty");
harness.edit_root_widget(|mut stack| {
IndexedStack::add_child(&mut stack, button::Button::new("A").with_auto_id());
IndexedStack::add_child(&mut stack, Button::with_text("A").with_auto_id());
});
assert_render_snapshot!(harness, "indexed_stack_single");
harness.edit_root_widget(|mut stack| {
IndexedStack::add_child(&mut stack, button::Button::new("B").with_auto_id());
IndexedStack::add_child(&mut stack, button::Button::new("C").with_auto_id());
IndexedStack::add_child(&mut stack, button::Button::new("D").with_auto_id());
IndexedStack::add_child(&mut stack, Button::with_text("B").with_auto_id());
IndexedStack::add_child(&mut stack, Button::with_text("C").with_auto_id());
IndexedStack::add_child(&mut stack, Button::with_text("D").with_auto_id());
});
assert_render_snapshot!(harness, "indexed_stack_single"); // the active child should not change
@ -318,9 +318,9 @@ mod tests {
#[test]
fn test_widget_removal_and_modification() {
let widget = IndexedStack::new()
.with_child(button::Button::new("A").with_auto_id())
.with_child(button::Button::new("B").with_auto_id())
.with_child(button::Button::new("C").with_auto_id())
.with_child(Button::with_text("A").with_auto_id())
.with_child(Button::with_text("B").with_auto_id())
.with_child(Button::with_text("C").with_auto_id())
.with_active_child(1)
.with_auto_id();
let window_size = Size::new(50.0, 50.0);
@ -343,7 +343,7 @@ mod tests {
// Add another widget at the end
harness.edit_root_widget(|mut stack| {
IndexedStack::add_child(&mut stack, button::Button::new("D").with_auto_id());
IndexedStack::add_child(&mut stack, Button::with_text("D").with_auto_id());
});
assert_render_snapshot!(harness, "indexed_stack_builder_removed_widget"); // Should not change
@ -355,8 +355,8 @@ mod tests {
// Insert back the first two at the start
harness.edit_root_widget(|mut stack| {
IndexedStack::insert_child(&mut stack, 0, button::Button::new("A").with_auto_id());
IndexedStack::insert_child(&mut stack, 1, button::Button::new("B").with_auto_id());
IndexedStack::insert_child(&mut stack, 0, Button::with_text("A").with_auto_id());
IndexedStack::insert_child(&mut stack, 1, Button::with_text("B").with_auto_id());
});
assert_render_snapshot!(harness, "indexed_stack_builder_new_widget"); // Should not change

View File

@ -493,7 +493,7 @@ mod tests {
use crate::widgets::{Button, Flex, SizedBox};
fn button(text: &'static str) -> impl Widget {
SizedBox::new(Button::new(text).with_auto_id())
SizedBox::new(Button::with_text(text).with_auto_id())
.width(70.0)
.height(40.0)
}
@ -569,7 +569,7 @@ mod tests {
Flex::column()
.with_spacer(500.0)
.with_child(NewWidget::new_with_id(
Button::new("Fully visible"),
Button::with_text("Fully visible"),
button_id,
))
.with_spacer(500.0)

View File

@ -60,7 +60,7 @@ fn propagate_hovered() {
Flex::column()
.with_spacer(100.0)
.with_child(NewWidget::new_with_id(
Button::new("hovered").record(&button_rec),
Button::with_text("hovered").record(&button_rec),
button,
))
.with_spacer(10.0)
@ -162,7 +162,9 @@ fn update_hovered_on_mouse_leave() {
let button_rec = Recording::default();
let widget = Button::new("hello").record(&button_rec).with_id(button_id);
let widget = Button::with_text("hello")
.record(&button_rec)
.with_id(button_id);
let mut harness = TestHarness::create(default_property_set(), widget);
@ -245,7 +247,7 @@ fn get_pointer_events_while_active() {
empty_2,
))
.with_child(NewWidget::new_with_id(
Button::new("hello").record(&button_rec),
Button::with_text("hello").record(&button_rec),
button,
))
.with_id(root);
@ -325,7 +327,7 @@ fn automatically_lose_pointer_on_pointer_lost() {
empty,
))
.with_child(NewWidget::new_with_id(
Button::new("hello").record(&button_rec),
Button::with_text("hello").record(&button_rec),
button,
))
.with_id(root);

View File

@ -56,7 +56,7 @@ fn transforms_translation_rotation() {
fn transforms_pointer_events() {
let transformed_widget = NewWidget::new_with_options(
blue_box(ZStack::new().with_child(
Button::new("Should be pressed").with_auto_id(),
Button::with_text("Should be pressed").with_auto_id(),
UnitPoint::BOTTOM_RIGHT,
)),
WidgetOptions {

View File

@ -88,7 +88,11 @@ fn main() {
.with_child(NewWidget::new(
Flex::row()
.with_flex_child(TextInput::new("").with_auto_id(), 1.0)
.with_child(Button::new("Add task").with_auto_id()),
.with_child(
Button::new(
Label::new("Add task").with_auto_id()
).with_auto_id()
),
))
.with_spacer(WIDGET_SPACING)
.with_auto_id(),

View File

@ -188,7 +188,7 @@ fn op_button_with_label(op: char, label: String) -> NewWidget<Button> {
const BLUE: Color = Color::from_rgb8(0x00, 0x8d, 0xdd);
const LIGHT_BLUE: Color = Color::from_rgb8(0x5c, 0xc4, 0xff);
let button = Button::from_label(
let button = Button::new(
Label::new(label)
.with_style(StyleProperty::FontSize(24.))
.with_auto_id(),
@ -216,7 +216,7 @@ fn digit_button(digit: u8) -> NewWidget<Button> {
const GRAY: Color = Color::from_rgb8(0x3a, 0x3a, 0x3a);
const LIGHT_GRAY: Color = Color::from_rgb8(0x71, 0x71, 0x71);
let button = Button::from_label(
let button = Button::new(
Label::new(format!("{digit}"))
.with_style(StyleProperty::FontSize(24.))
.with_auto_id(),

View File

@ -50,7 +50,7 @@ impl AppDriver for Driver {
}
fn grid_button(params: GridParams) -> Button {
Button::new(format!(
Button::with_text(format!(
"X: {}, Y: {}, W: {}, H: {}",
params.x, params.y, params.width, params.height
))

View File

@ -45,7 +45,7 @@ fn main() {
.with_style(StyleProperty::FontSize(32.0))
// Ideally there's be an Into in Parley for this
.with_style(StyleProperty::FontWeight(FontWeight::BOLD));
let button = Button::new("Say hello");
let button = Button::with_text("Say hello");
// Arrange the two widgets vertically, with some padding
let main_widget = Flex::column()

View File

@ -64,7 +64,7 @@ pub fn make_widget_tree() -> NewWidget<impl Widget> {
.with_child(NewWidget::new_with_props(
Flex::row()
.with_flex_child(TextInput::new("").with_auto_id(), 1.0)
.with_child(Button::new("Add task").with_auto_id()),
.with_child(Button::with_text("Add task").with_auto_id()),
Properties::new().with(Padding::all(WIDGET_SPACING)),
))
.with_spacer(WIDGET_SPACING)

View File

@ -65,7 +65,11 @@
//! .with_child(NewWidget::new(
//! Flex::row()
//! .with_flex_child(TextInput::new("").with_auto_id(), 1.0)
//! .with_child(Button::new("Add task").with_auto_id()),
//! .with_child(
//! Button::new(
//! Label::new("Add task").with_auto_id()
//! ).with_auto_id()
//! ),
//! ))
//! .with_spacer(WIDGET_SPACING)
//! .with_auto_id(),

View File

@ -150,7 +150,7 @@ where
View::<State, Action, _>::build(&self.label, ctx, app_state)
});
ctx.with_leaf_action_widget(|ctx| {
let mut pod = ctx.create_pod(widgets::Button::from_label(child.new_widget));
let mut pod = ctx.create_pod(widgets::Button::new(child.new_widget));
pod.new_widget.properties = self.properties.build_properties();
pod.new_widget.options.disabled = self.disabled;
pod
@ -176,7 +176,7 @@ where
&prev.label,
state,
ctx,
widgets::Button::label_mut(&mut element),
widgets::Button::child_mut(&mut element).downcast(),
app_state,
);
});
@ -194,7 +194,7 @@ where
&self.label,
&mut (),
ctx,
widgets::Button::label_mut(&mut element),
widgets::Button::child_mut(&mut element).downcast(),
app_state,
);
});
@ -212,7 +212,7 @@ where
Some(LABEL_VIEW_ID) => self.label.message(
&mut (),
message,
widgets::Button::label_mut(&mut element),
widgets::Button::child_mut(&mut element).downcast(),
app_state,
),
None => match message.take_message::<ButtonPress>() {