From 620ab2dc44cd3ea9c6c3c61ca0086fee42baef37 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sat, 16 Nov 2024 16:27:51 -0800 Subject: [PATCH] Pile widget Closes #208 --- CHANGELOG.md | 3 + examples/pile.rs | 55 +++++++++++ src/widget.rs | 18 ++++ src/widgets.rs | 1 + src/widgets/pile.rs | 225 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 302 insertions(+) create mode 100644 examples/pile.rs create mode 100644 src/widgets/pile.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index eceffaea7..928a58b97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -323,6 +323,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 performing arbitrary `wgpu` drawing operations when rendering. See the `shaders.rs` example for an example on how to use this to render into a Canvas with a custom shader. +- `Pile` is a new widget that shows one of many widgets. `PiledWidget` handles + are returned for each widget pushed into a pile. These handles can be used to + show or close a specific widget in a pile. [139]: https://github.com/khonsulabs/cushy/issues/139 diff --git a/examples/pile.rs b/examples/pile.rs new file mode 100644 index 000000000..54d30b0d0 --- /dev/null +++ b/examples/pile.rs @@ -0,0 +1,55 @@ +use cushy::value::Dynamic; +use cushy::widget::{MakeWidget, WidgetList}; +use cushy::widgets::input::InputValue; +use cushy::widgets::pile::Pile; +use cushy::Run; + +fn main() -> cushy::Result { + let pile = Pile::default(); + let mut counter = 0; + let buttons = Dynamic::::default(); + buttons.lock().push("+".into_button().on_click({ + let buttons = buttons.clone(); + let pile = pile.clone(); + move |_| { + counter += 1; + + let pending_section = pile.new_pending(); + let handle = pending_section.clone(); + let button = format!("{counter}") + .into_button() + .on_click({ + let section = handle.clone(); + move |_| section.show(true) + }) + .make_widget(); + let button_id = button.id(); + + pending_section.finish( + Dynamic::new(format!("Section {counter}")) + .into_input() + .and("Close Section".into_button().on_click({ + let buttons = buttons.clone(); + move |_| { + // Remove the section widget. + handle.remove(); + // Remove the button. + buttons.lock().retain(|button| button.id() != button_id); + } + })) + .into_rows() + .centered(), + ); + let mut buttons = buttons.lock(); + let index = buttons.len() - 1; + buttons.insert(index, button) + } + })); + + buttons + .into_columns() + .and(pile.centered().expand()) + .into_rows() + .expand() + .run() +} diff --git a/src/widget.rs b/src/widget.rs index e07b9dc34..2d891083f 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -2148,6 +2148,24 @@ impl WidgetList { self.ordered.insert(index, widget.make_widget()); } + /// Removes the widget at `index`. + /// + /// # Panics + /// + /// This function will panic if `index` is out of the range of this + /// collection. + pub fn remove(&mut self, index: usize) -> WidgetInstance { + self.ordered.remove(index) + } + + /// Retains all widgets where `func` returns true. + pub fn retain(&mut self, func: F) + where + F: FnMut(&WidgetInstance) -> bool, + { + self.ordered.retain(func); + } + /// Extends this collection with the contents of `iter`. pub fn extend(&mut self, iter: Iter) where diff --git a/src/widgets.rs b/src/widgets.rs index 4c5604db5..9642f4943 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -22,6 +22,7 @@ pub mod layers; pub mod list; pub mod menu; mod mode_switch; +pub mod pile; pub mod progress; pub mod radio; mod resize; diff --git a/src/widgets/pile.rs b/src/widgets/pile.rs new file mode 100644 index 000000000..df5ba3f6d --- /dev/null +++ b/src/widgets/pile.rs @@ -0,0 +1,225 @@ +//! A widget that piles multiple widgets into a single area. + +use std::collections::VecDeque; +use std::sync::Arc; + +use ahash::AHashMap; +use alot::{LotId, Lots}; +use figures::units::UPx; +use figures::{IntoSigned, Rect, Size}; +use intentional::Assert; + +use crate::context::{EventContext, GraphicsContext, LayoutContext}; +use crate::value::{Dynamic, DynamicRead, DynamicReader}; +use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetRef, WidgetTag}; +use crate::ConstraintLimit; + +/// A pile of widgets that shows the top widget. +/// +/// This is a lower level widget that is similar to a +/// [`Switcher`](super::switcher::Switcher) except that all widgets held in the +/// pile remain mounted in the window when not active. This allows widgets to +/// retain information stored in a [`WindowLocal`](crate::window::WindowLocal). +#[derive(Debug, Clone, Default)] +pub struct Pile { + data: Dynamic, +} + +#[derive(Default, Debug)] +struct PileData { + widgets: Lots>, + visible: VecDeque, + focus_visible: bool, +} + +impl PileData { + fn hide_id(&mut self, to_remove: LotId) { + let Some((index, _)) = self + .visible + .iter() + .enumerate() + .find(|(_index, id)| **id == to_remove) + else { + return; + }; + self.visible.remove(index); + } +} + +impl Pile { + /// Returns a placeholder that can be used to show/close a piled widget + /// before it has been constructed. + #[must_use] + pub fn new_pending(&self) -> PendingPiledWidget { + let mut pile = self.data.lock(); + let id = pile.widgets.push(None); + PendingPiledWidget(Some(PiledWidget(Arc::new(PiledWidgetData { + pile: self.clone(), + id, + })))) + } + + /// Adds a new widget to the pile. + /// + /// If this is the first widget, it will become visible automatically. + /// Otherwise, it will be placed at the bottom of the pile. + /// + /// When the last clone of the returned [`PiledWidget`] is dropped, `widget` + /// will be removed from the pile. If it is the currently visible widget, + /// the next widget in the pile will be made visible. + pub fn push(&self, widget: impl MakeWidget) -> PiledWidget { + self.new_pending().finish(widget) + } +} + +impl MakeWidgetWithTag for Pile { + fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance { + WidgetPile { + pile: self.data.into_reader(), + widgets: AHashMap::new(), + last_visible: None, + } + .make_with_tag(tag) + } +} + +#[derive(Debug)] +struct WidgetPile { + pile: DynamicReader, + widgets: AHashMap, + last_visible: Option, +} + +impl WidgetPile { + fn synchronize_widgets(&mut self) { + let pile = self.pile.read(); + for (id, widget) in pile.widgets.entries() { + if let Some(widget) = widget.as_ref() { + self.widgets + .entry(id) + .or_insert_with(|| WidgetRef::new(widget.clone())); + } + } + + self.widgets.retain(|id, _| pile.widgets.get(*id).is_some()); + } +} + +impl Widget for WidgetPile { + fn layout( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_>, + ) -> Size { + context.invalidate_when_changed(&self.pile); + self.synchronize_widgets(); + let pile = self.pile.read(); + let visible = pile.visible.front().copied(); + let size = if let Some(id) = visible { + let visible = self + .widgets + .get_mut(&id) + .expect("visible widget") + .mounted(context); + let mut child_context = context.for_other(&visible); + if pile.focus_visible && self.last_visible != Some(id) { + child_context.focus(); + } + let size = child_context.layout(available_space); + drop(child_context); + context.set_child_layout(&visible, Rect::from(size).into_signed()); + size + } else { + available_space.map(ConstraintLimit::min) + }; + + self.last_visible = visible; + + size + } + + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { + context.invalidate_when_changed(&self.pile); + self.synchronize_widgets(); + let pile = self.pile.read(); + if let Some(visible) = pile.visible.front() { + let visible = self + .widgets + .get_mut(visible) + .expect("visible widget") + .mounted(context); + context.for_other(&visible).redraw(); + } + } + + fn unmounted(&mut self, context: &mut EventContext<'_>) { + for widget in self.widgets.values_mut() { + widget.unmount_in(context); + } + } +} + +/// A placeholder for a widget in a [`Pile`]. +pub struct PendingPiledWidget(Option); + +impl PendingPiledWidget { + /// Place `widget` in the pile and returns a handle to the placed widget. + #[allow(clippy::must_use_candidate)] + pub fn finish(mut self, widget: impl MakeWidget) -> PiledWidget { + let piled = self.0.take().assert("finished called once"); + let mut pile = piled.0.pile.data.lock(); + pile.widgets[piled.0.id] = Some(widget.make_widget()); + pile.visible.push_back(piled.0.id); + drop(pile); + + piled + } +} + +impl std::ops::Deref for PendingPiledWidget { + type Target = PiledWidget; + + fn deref(&self) -> &Self::Target { + self.0.as_ref().expect("accessed after finished") + } +} + +/// A widget that has been added to a [`Pile`]. +#[derive(Clone, Debug)] +pub struct PiledWidget(Arc); + +impl PiledWidget { + /// Shows this widget in its pile. + /// + /// If `focus` is true, the widget will be focused when shown. + pub fn show(&self, focus: bool) { + let mut pile = self.0.pile.data.lock(); + pile.hide_id(self.0.id); + pile.visible.push_front(self.0.id); + pile.focus_visible = focus; + } + + /// Removes this widget from the pile. + pub fn remove(&self) { + let mut pile = self.0.pile.data.lock(); + if pile.visible.front() == Some(&self.0.id) { + pile.focus_visible = false; + } + pile.hide_id(self.0.id); + pile.widgets.remove(self.0.id); + } +} + +#[derive(Clone, Debug)] +struct PiledWidgetData { + pile: Pile, + id: LotId, +} + +impl Drop for PiledWidgetData { + fn drop(&mut self) { + let mut pile = self.pile.data.lock(); + pile.hide_id(self.id); + pile.widgets.remove(self.id); + } +}