Skip to content

Commit

Permalink
Pile widget
Browse files Browse the repository at this point in the history
Closes #208
  • Loading branch information
ecton committed Nov 17, 2024
1 parent 1f16cd3 commit 620ab2d
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions examples/pile.rs
Original file line number Diff line number Diff line change
@@ -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::<WidgetList>::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()
}
18 changes: 18 additions & 0 deletions src/widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F>(&mut self, func: F)
where
F: FnMut(&WidgetInstance) -> bool,
{
self.ordered.retain(func);
}

/// Extends this collection with the contents of `iter`.
pub fn extend<T, Iter>(&mut self, iter: Iter)
where
Expand Down
1 change: 1 addition & 0 deletions src/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
225 changes: 225 additions & 0 deletions src/widgets/pile.rs
Original file line number Diff line number Diff line change
@@ -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<PileData>,
}

#[derive(Default, Debug)]
struct PileData {
widgets: Lots<Option<WidgetInstance>>,
visible: VecDeque<LotId>,
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<PileData>,
widgets: AHashMap<LotId, WidgetRef>,
last_visible: Option<LotId>,
}

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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> {
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<PiledWidget>);

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<PiledWidgetData>);

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);
}
}

0 comments on commit 620ab2d

Please sign in to comment.