From 1c8d4e0176739d10ef3b2c2b5db7340514c58d97 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 8 May 2024 08:38:26 -0700 Subject: [PATCH] ButtonClick + Overlays at locations The overlay example now supports right-clicking to open overlays at the clicked location, showing how a context menu widget can begin being built. --- CHANGELOG.md | 3 + examples/animation.rs | 3 +- examples/background-tasks.rs | 2 +- examples/buttonception.rs | 6 +- examples/checkbox.rs | 2 +- examples/contacts.rs | 2 +- examples/debug-window.rs | 2 +- examples/login.rs | 2 +- examples/multi-window.rs | 4 +- examples/overlays.rs | 23 +++++-- examples/theme.rs | 2 +- examples/validation.rs | 4 +- src/debug.rs | 12 ++-- src/widgets/button.rs | 38 ++++++++--- src/widgets/checkbox.rs | 2 +- src/widgets/grid.rs | 6 ++ src/widgets/input.rs | 3 - src/widgets/layers.rs | 121 +++++++++++++++++++++++++---------- src/widgets/radio.rs | 2 +- src/widgets/select.rs | 2 +- 20 files changed, 165 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 321e547..459e108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `PlatformWindowImplementation::set_cursor_icon` and `PlatformWindow::set_cursor_icon` have been renamed to `set_cursor` and accept `winit` 0.30's new `Cursor` type. +- `Button::on_click` now takes a `Option` structure. When this + value is provided, information about the mouse click that caused the event is + provided. ### Fixed diff --git a/examples/animation.rs b/examples/animation.rs index 8e7c867..90fc768 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -3,6 +3,7 @@ use std::time::Duration; use cushy::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn}; use cushy::value::{Destination, Dynamic}; use cushy::widget::MakeWidget; +use cushy::widgets::button::ButtonClick; use cushy::widgets::progress::Progressable; use cushy::{Run, WithClone}; use figures::units::Lp; @@ -41,7 +42,7 @@ fn animate_to( animation: &Dynamic, value: &Dynamic, target: u8, -) -> impl FnMut(()) { +) -> impl FnMut(Option) { (animation, value).with_clone(|(animation, value)| { move |_| { // Here we use spawn to schedule the animation, which returns an diff --git a/examples/background-tasks.rs b/examples/background-tasks.rs index 885eb2b..f85dc86 100644 --- a/examples/background-tasks.rs +++ b/examples/background-tasks.rs @@ -27,7 +27,7 @@ fn main() -> cushy::Result { .into_button() .on_click({ let task = dynamic.clone(); - move |()| { + move |_| { let background_task = Task::default(); spawn_background_thread(&background_task.progress, &task); task.set(Some(background_task)); diff --git a/examples/buttonception.rs b/examples/buttonception.rs index c5f8a8a..c87a4a8 100644 --- a/examples/buttonception.rs +++ b/examples/buttonception.rs @@ -29,7 +29,7 @@ fn main() -> cushy::Result { .into_button() .on_click({ let clicked_button = clicked_button.clone(); - move |()| clicked_button.set("inner button clicked") + move |_| clicked_button.set("inner button clicked") }) .with(&ButtonHoverBackground, Color::RED) .with(&ButtonHoverForeground, Color::WHITE); @@ -40,7 +40,7 @@ fn main() -> cushy::Result { .into_button() .on_click({ let clicked_button = clicked_button.clone(); - move |()| clicked_button.set("middle button clicked") + move |_| clicked_button.set("middle button clicked") }); let outer_button = "Yo dawg!" @@ -49,7 +49,7 @@ fn main() -> cushy::Result { .into_button() .on_click({ let clicked_button = clicked_button.clone(); - move |()| clicked_button.set("outer button clicked") + move |_| clicked_button.set("outer button clicked") }); outer_button diff --git a/examples/checkbox.rs b/examples/checkbox.rs index f519c26..8bb7c58 100644 --- a/examples/checkbox.rs +++ b/examples/checkbox.rs @@ -10,7 +10,7 @@ fn main() -> cushy::Result { checkbox_state .clone() .to_checkbox(label) - .and("Maybe".into_button().on_click(move |()| { + .and("Maybe".into_button().on_click(move |_| { checkbox_state.set(CheckboxState::Indeterminant); })) .into_columns() diff --git a/examples/contacts.rs b/examples/contacts.rs index 0d797d4..92229d6 100644 --- a/examples/contacts.rs +++ b/examples/contacts.rs @@ -87,7 +87,7 @@ fn edit_contact_form(contact: &Contact, db: &Dynamic>) -> .on_click({ let contact_id = contact.id; let db = db.clone(); - move |()| { + move |_| { let mut db = db.lock(); let contact = db.get_mut(&contact_id).expect("missing contact"); contact.first_name = first.get(); diff --git a/examples/debug-window.rs b/examples/debug-window.rs index f812cfe..2f099a1 100644 --- a/examples/debug-window.rs +++ b/examples/debug-window.rs @@ -19,7 +19,7 @@ fn main() -> cushy::Result { let info = info.clone(); let window_count = window_count.clone(); let total_windows = total_windows.clone(); - move |()| open_a_window(&window_count, &total_windows, &info, &mut app) + move |_| open_a_window(&window_count, &total_windows, &info, &mut app) }) .make_widget(); diff --git a/examples/login.rs b/examples/login.rs index eb7688e..d54e1c0 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -64,7 +64,7 @@ fn main() -> cushy::Result { .and( "Log In" .into_button() - .on_click(validations.when_valid(move |()| { + .on_click(validations.when_valid(move |_| { println!("Welcome, {}", username.get()); exit(0); })) diff --git a/examples/multi-window.rs b/examples/multi-window.rs index 5c12b93..64fa4e8 100644 --- a/examples/multi-window.rs +++ b/examples/multi-window.rs @@ -51,7 +51,7 @@ fn open_window_button( let open_windows = open_windows.clone(); let counter = counter.clone(); let texture = texture.clone(); - "Open Another Window".into_button().on_click(move |()| { + "Open Another Window".into_button().on_click(move |_| { open_another_window(&mut app, &open_windows, &counter, &texture); }) } @@ -82,7 +82,7 @@ fn open_another_window( .and( "Close" .into_button() - .on_click(move |()| handle.request_close()), + .on_click(move |_| handle.request_close()), ) .into_rows() .centered(), diff --git a/examples/overlays.rs b/examples/overlays.rs index e9fc816..472a8c1 100644 --- a/examples/overlays.rs +++ b/examples/overlays.rs @@ -4,6 +4,7 @@ use cushy::widgets::layers::{OverlayBuilder, OverlayLayer}; use cushy::Run; use figures::units::Lp; use figures::{Point, Zero}; +use kludgine::app::winit::event::MouseButton; use kludgine::Color; use rand::{thread_rng, Rng}; @@ -55,10 +56,20 @@ fn show_overlay_button( direction_func: impl for<'a> Fn(OverlayBuilder<'a>) -> OverlayBuilder<'a> + Send + 'static, ) -> impl MakeWidget { let overlay = overlay.clone(); - label.into_button().on_click(move |()| { - direction_func(overlay.build_overlay(test_widget(&overlay, false))) - .hide_on_unhover() - .show() - .forget(); - }) + let button_tag = WidgetTag::unique(); + let button_id = button_tag.id(); + label + .into_button() + .on_click(move |click| { + let overlay = overlay.build_overlay(test_widget(&overlay, false)); + let overlay = match click { + Some(click) if click.mouse_button == MouseButton::Right => { + overlay.above(button_id).at(click.window_location) + } + _ => direction_func(overlay), + }; + + overlay.hide_on_unhover().show().forget(); + }) + .make_with_tag(button_tag) } diff --git a/examples/theme.rs b/examples/theme.rs index 3e8d112..72891fe 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -80,7 +80,7 @@ fn main() -> cushy::Result { .and(editors.neutral_variant.1) .and("Copy to Clipboard".into_button().on_click({ let cushy = app.cushy().clone(); - move |()| { + move |_| { if let Some(mut clipboard) = cushy.clipboard_guard() { let builder = color_scheme_builder.get(); let mut source = String::default(); diff --git a/examples/validation.rs b/examples/validation.rs index a7a29d8..b620f90 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -22,13 +22,13 @@ fn main() -> cushy::Result { .and( "Submit" .into_button() - .on_click(validations.clone().when_valid(move |()| { + .on_click(validations.clone().when_valid(move |_| { println!( "Success! This callback only happens when all associated validations are valid" ); })), ) - .and("Reset".into_button().on_click(move |()| { + .and("Reset".into_button().on_click(move |_| { let _value = text.take(); validations.reset(); })) diff --git a/src/debug.rs b/src/debug.rs index 6f6db1b..db18ecc 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -52,7 +52,7 @@ impl DebugContext { let id = self.section.map_ref(|section| { section.values.lock().push(Box::new(RegisteredValue { label: label.into(), - value: reader.clone(), + _value: reader.clone(), widget: make_observer(value.weak_clone()).make_widget(), })) }); @@ -145,13 +145,13 @@ impl Drop for DebugContext { trait Observable: Send { fn label(&self) -> &str; - fn alive(&self) -> bool; + // fn alive(&self) -> bool; fn widget(&self) -> &WidgetInstance; } struct RegisteredValue { label: String, - value: DynamicReader, + _value: DynamicReader, widget: WidgetInstance, } @@ -163,9 +163,9 @@ where &self.label } - fn alive(&self) -> bool { - self.value.connected() - } + // fn alive(&self) -> bool { + // self.value.connected() + // } fn widget(&self) -> &WidgetInstance { &self.widget diff --git a/src/widgets/button.rs b/src/widgets/button.rs index b0d3a4a..7f5ea6e 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -30,7 +30,7 @@ pub struct Button { /// The label to display on the button. pub content: WidgetRef, /// The callback that is invoked when the button is clicked. - pub on_click: Option>, + pub on_click: Option>>, /// The kind of button to draw. pub kind: Value, focusable: bool, @@ -158,7 +158,7 @@ impl Button { #[must_use] pub fn on_click(mut self, callback: F) -> Self where - F: FnMut(()) + Send + 'static, + F: FnMut(Option) + Send + 'static, { self.on_click = Some(Callback::new(callback)); self @@ -171,10 +171,10 @@ impl Button { self } - fn invoke_on_click(&mut self, context: &WidgetContext<'_>) { + fn invoke_on_click(&mut self, button: Option, context: &WidgetContext<'_>) { if context.enabled() { if let Some(on_click) = self.on_click.as_mut() { - on_click.invoke(()); + on_click.invoke(button); } } } @@ -461,7 +461,7 @@ impl Widget for Button { &mut self, location: Option>, _device_id: DeviceId, - _button: MouseButton, + button: MouseButton, context: &mut EventContext<'_>, ) { let window_local = self.per_window.entry(context).or_default(); @@ -470,12 +470,19 @@ impl Widget for Button { context.deactivate(); if let (true, Some(location)) = (self.focusable, location) { - if Rect::from(context.last_layout().expect("must have been rendered").size) - .contains(location) - { + let last_layout = context.last_layout().expect("must have been rendered"); + // let button_relative + if Rect::from(last_layout.size).contains(location) { context.focus(); - self.invoke_on_click(context); + self.invoke_on_click( + Some(ButtonClick { + mouse_button: button, + location, + window_location: location + last_layout.origin, + }), + context, + ); } } } @@ -533,7 +540,7 @@ impl Widget for Button { // If we have no buttons pressed, the event should fire on activate not // on deactivate. if window_local.buttons_pressed == 0 { - self.invoke_on_click(context); + self.invoke_on_click(None, context); } self.update_colors(context, true); } @@ -581,3 +588,14 @@ define_components! { ButtonDisabledOutline(Color, "disabled_outline_color", Color::CLEAR_BLACK) } } + +/// A mouse click in a [`Button`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct ButtonClick { + /// The mouse button that caused the event. + pub mouse_button: MouseButton, + /// The location relative to the button of the click. + pub location: Point, + /// The location relative to the window of the click. + pub window_location: Point, +} diff --git a/src/widgets/checkbox.rs b/src/widgets/checkbox.rs index 8ed906c..78a9d7c 100644 --- a/src/widgets/checkbox.rs +++ b/src/widgets/checkbox.rs @@ -59,7 +59,7 @@ impl MakeWidgetWithTag for Checkbox { .and(self.label) .into_columns() .into_button() - .on_click(move |()| { + .on_click(move |_| { let mut value = self.state.lock(); *value = !*value; }) diff --git a/src/widgets/grid.rs b/src/widgets/grid.rs index e747297..5a2a3ed 100644 --- a/src/widgets/grid.rs +++ b/src/widgets/grid.rs @@ -856,6 +856,12 @@ impl DerefMut for GridWidgets { #[derive(Debug, Eq, PartialEq)] pub struct GridSection([WidgetInstance; N]); +impl Default for GridSection<0> { + fn default() -> Self { + Self::new() + } +} + impl GridSection<0> { /// Returns an empty section. #[must_use] diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 7de7185..c5dd46a 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -1243,9 +1243,6 @@ where } } -#[derive(Debug, PartialEq, Eq)] -struct NotVisible(Point, usize); - #[derive(Clone, Copy)] struct BlinkState { visible: bool, diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 97b9dd9..63bef0c 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -149,7 +149,7 @@ impl OverlayLayer { layout: OverlayLayout { widget: WidgetRef::new(overlay), relative_to: None, - direction: Direction::Right, + positioning: Position::Relative(Direction::Right), requires_hover: false, on_dismiss: None, layout: None, @@ -395,26 +395,26 @@ impl OverlayState { context: &mut LayoutContext<'_, '_, '_, '_>, relative_to: WidgetId, ) -> Option> { - let direction = self.overlays[index].direction; + let positioning = self.overlays[index].positioning; let relative_to = relative_to.find_in(context)?.last_layout()?; let relative_to_unsigned = relative_to.into_unsigned(); - let constraints = match direction { - Direction::Up => Size::new( + let constraints = match positioning { + Position::Relative(Direction::Up) => Size::new( relative_to_unsigned.size.width, relative_to_unsigned.origin.y, ), - Direction::Down => Size::new( + Position::Relative(Direction::Down) => Size::new( relative_to_unsigned.size.width, available_space.height - relative_to_unsigned.origin.y - relative_to_unsigned.size.height, ), - Direction::Left => Size::new( + Position::Relative(Direction::Left) => Size::new( relative_to_unsigned.origin.x, relative_to_unsigned.size.height, ), - Direction::Right => Size::new( + Position::Relative(Direction::Right) => Size::new( available_space.width.saturating_sub( relative_to_unsigned .origin @@ -423,6 +423,7 @@ impl OverlayState { ), relative_to_unsigned.size.height, ), + Position::At(_) => available_space, }; let size = context @@ -430,26 +431,39 @@ impl OverlayState { .layout(constraints.map(ConstraintLimit::SizeToFit)) .into_signed(); - let mut layout_direction = direction; + let mut layout_direction = positioning; let mut layout; loop { - let origin = match layout_direction { - Direction::Up => Point::new( - relative_to.origin.x + relative_to.size.width / 2 - size.width / 2, - relative_to.origin.y - size.height, + let (origin, intersection_matters) = match layout_direction { + Position::Relative(Direction::Up) => ( + Point::new( + relative_to.origin.x + relative_to.size.width / 2 - size.width / 2, + relative_to.origin.y - size.height, + ), + true, ), - Direction::Down => Point::new( - relative_to.origin.x + relative_to.size.width / 2 - size.width / 2, - relative_to.origin.y + relative_to.size.height, + Position::Relative(Direction::Down) => ( + Point::new( + relative_to.origin.x + relative_to.size.width / 2 - size.width / 2, + relative_to.origin.y + relative_to.size.height, + ), + true, ), - Direction::Left => Point::new( - relative_to.origin.x - size.width, - relative_to.origin.y + relative_to.size.height / 2 - size.height / 2, + Position::Relative(Direction::Left) => ( + Point::new( + relative_to.origin.x - size.width, + relative_to.origin.y + relative_to.size.height / 2 - size.height / 2, + ), + true, ), - Direction::Right => Point::new( - relative_to.origin.x + relative_to.size.width, - relative_to.origin.y + relative_to.size.height / 2 - size.height / 2, + Position::Relative(Direction::Right) => ( + Point::new( + relative_to.origin.x + relative_to.size.width, + relative_to.origin.y + relative_to.size.height / 2 - size.height / 2, + ), + true, ), + Position::At(pt) => (pt, false), }; layout = Rect::new(origin.max(Point::ZERO), size); @@ -462,16 +476,27 @@ impl OverlayState { layout.origin.y -= bottom_right.y - available_space.height.into_signed(); } - if layout.intersects(&relative_to) || self.layout_intersects(index, &layout, context) { - layout_direction = layout_direction.next_clockwise(); - if layout_direction == direction { - // No layout worked optimally. + if intersection_matters + && (layout.intersects(&relative_to) + || self.layout_intersects(index, &layout, context)) + { + if let Some(next_direction) = layout_direction.next_clockwise() { + if layout_direction == positioning { + // No layout worked optimally. + break; + } + layout_direction = next_direction; + } else { break; } } else { break; } } + + // TODO check to ensure the widget is fully on-window, otherwise attempt + // to shift it to become visible. + Some(layout) } @@ -516,7 +541,7 @@ impl OverlayState { if let Some(relative_to) = self.overlays[index].relative_to { self.layout_overlay_relative(index, widget, available_space, context, relative_to) } else { - let direction = self.overlays[index].direction; + let direction = self.overlays[index].positioning; let size = context .for_other(widget) .layout(available_space.map(ConstraintLimit::SizeToFit)) @@ -525,22 +550,23 @@ impl OverlayState { let available_space = available_space.into_signed(); let origin = match direction { - Direction::Up => Point::new( + Position::Relative(Direction::Up) => Point::new( available_space.width / 2, (available_space.height - size.height) / 2, ), - Direction::Down => Point::new( + Position::Relative(Direction::Down) => Point::new( available_space.width / 2, available_space.height / 2 + size.height / 2, ), - Direction::Right => Point::new( + Position::Relative(Direction::Right) => Point::new( available_space.width / 2 + size.width / 2, available_space.height / 2, ), - Direction::Left => Point::new( + Position::Relative(Direction::Left) => Point::new( (available_space.width - size.width) / 2, available_space.height / 2, ), + Position::At(pt) => pt, }; Some(Rect::new(origin, size)) @@ -592,7 +618,14 @@ impl OverlayBuilder<'_> { #[must_use] pub fn near(mut self, id: WidgetId, direction: Direction) -> Self { self.layout.relative_to = Some(id); - self.layout.direction = direction; + self.layout.positioning = Position::Relative(direction); + self + } + + /// Shows this overlay at a specified window `location`. + #[must_use] + pub fn at(mut self, location: Point) -> Self { + self.layout.positioning = Position::At(location); self } @@ -632,7 +665,7 @@ struct OverlayLayout { widget: WidgetRef, opacity: Dynamic, relative_to: Option, - direction: Direction, + positioning: Position, requires_hover: bool, layout: Option>, on_dismiss: Option>>, @@ -654,7 +687,7 @@ impl PartialEq for OverlayLayout { self.widget == other.widget && self.opacity == other.opacity && self.relative_to == other.relative_to - && self.direction == other.direction + && self.positioning == other.positioning && self.requires_hover == other.requires_hover && self.layout == other.layout && match (&self.on_dismiss, &other.on_dismiss) { @@ -665,6 +698,26 @@ impl PartialEq for OverlayLayout { } } +/// An overlay position. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Position { + /// Relative to the parent in a given direction. + Relative(Direction), + /// At a window coordinate. + At(Point), +} + +impl Position { + /// Returns the next direction when rotating clockwise. + #[must_use] + pub fn next_clockwise(&self) -> Option { + match self { + Self::Relative(direction) => Some(Self::Relative(direction.next_clockwise())), + Self::At(_) => None, + } + } +} + /// A relative direction. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum Direction { @@ -674,7 +727,7 @@ pub enum Direction { Right, /// Positive along the Y axis. Down, - /// Legative along the X axis. + /// Negative along the X axis. Left, } diff --git a/src/widgets/radio.rs b/src/widgets/radio.rs index f3c2aad..2cfe937 100644 --- a/src/widgets/radio.rs +++ b/src/widgets/radio.rs @@ -62,7 +62,7 @@ where .and(self.label) .into_columns() .into_button() - .on_click(move |()| { + .on_click(move |_| { self.state.set(self.value.clone()); }) .kind(self.kind) diff --git a/src/widgets/select.rs b/src/widgets/select.rs index d27f790..6e19547 100644 --- a/src/widgets/select.rs +++ b/src/widgets/select.rs @@ -73,7 +73,7 @@ where }); self.label .into_button() - .on_click(move |()| { + .on_click(move |_| { self.state.set(self.value.clone()); }) .kind(kind)