all_is_cubes_ui/vui/widgets/button/
widget.rs

1//! Public API types and functions for button widgets.
2
3use all_is_cubes::universe;
4use alloc::sync::Arc;
5use core::fmt;
6use core::hash::Hash;
7
8use exhaust::Exhaust;
9
10use all_is_cubes::arcstr::ArcStr;
11use all_is_cubes::block::Block;
12use all_is_cubes::inv::EphemeralOpaque;
13use all_is_cubes::linking;
14use all_is_cubes::listen;
15
16use crate::vui;
17use crate::vui::widgets::{BoxStyle, WidgetBlocks, WidgetTheme};
18
19use super::Action;
20
21// -------------------------------------------------------------------------------------------------
22
23/// Common elements of button widgets.
24/// A button widget is a widget that displays a single clickable shape that can have a finite set
25/// of possible appearances (on/off, pressed, etc).
26#[derive(Clone, Debug)]
27pub(in crate::vui::widgets::button) struct ButtonCommon<St> {
28    /// Button shape, indicating what kind of button it is.
29    pub(in crate::vui::widgets::button) shape: linking::Provider<St, BoxStyle>,
30
31    /// Label to be put on top of the shape.
32    pub(in crate::vui::widgets::button) label: ButtonLabel,
33}
34
35impl<St: Clone + Eq + Hash + Exhaust + fmt::Debug> ButtonCommon<St> {
36    fn new(shape: &linking::Provider<St, Block>, label: ButtonLabel) -> Self {
37        let shape = shape.map(|_, base_multiblock| BoxStyle::from_nine_and_thin(base_multiblock));
38        Self { shape, label }
39    }
40}
41
42impl<St: Eq + Hash + universe::VisitHandles> universe::VisitHandles for ButtonCommon<St> {
43    fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
44        let Self { shape, label } = self;
45        shape.visit_handles(visitor);
46        label.visit_handles(visitor);
47    }
48}
49
50// -------------------------------------------------------------------------------------------------
51
52/// What is displayed on the face of a button widget.
53#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
54#[non_exhaustive]
55pub struct ButtonLabel {
56    /// Picture stuck to the front face of the button. It should be flat against the -Z face and
57    /// only a few voxels thick.
58    /// Specific button types have more specific requirements for margin.
59    pub icon: Option<Block>,
60
61    /// Text to display.
62    pub text: Option<vui::widgets::Label>,
63}
64
65impl From<Block> for ButtonLabel {
66    fn from(icon: Block) -> Self {
67        ButtonLabel {
68            icon: Some(icon),
69            text: None,
70        }
71    }
72}
73impl From<vui::widgets::Label> for ButtonLabel {
74    fn from(text: vui::widgets::Label) -> Self {
75        ButtonLabel {
76            icon: None,
77            text: Some(text),
78        }
79    }
80}
81impl From<ArcStr> for ButtonLabel {
82    fn from(string: ArcStr) -> Self {
83        ButtonLabel {
84            icon: None,
85            text: Some(vui::widgets::Label::new(string)),
86        }
87    }
88}
89
90impl universe::VisitHandles for ButtonLabel {
91    fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
92        let Self { icon, text } = self;
93        icon.visit_handles(visitor);
94        text.visit_handles(visitor);
95    }
96}
97
98// -------------------------------------------------------------------------------------------------
99// The button widget types themselves.
100
101/// A single-block button that reacts to activations (clicks) but does not change
102/// otherwise.
103#[derive(Clone, Debug)]
104pub struct ActionButton {
105    pub(in crate::vui::widgets::button) common: ButtonCommon<ButtonVisualState>,
106    pub(in crate::vui::widgets::button) action: Action,
107}
108
109impl ActionButton {
110    #[allow(missing_docs)] // TODO
111    pub fn new(
112        label: impl Into<ButtonLabel>,
113        theme: &WidgetTheme,
114        action: impl Fn() + Send + Sync + 'static,
115    ) -> Arc<Self> {
116        Arc::new(Self {
117            common: ButtonCommon::new(
118                &theme.widget_blocks.subset(WidgetBlocks::ActionButton),
119                label.into(),
120            ),
121            action: EphemeralOpaque::new(Arc::new(action)),
122        })
123    }
124}
125
126impl vui::Layoutable for ActionButton {
127    fn requirements(&self) -> vui::LayoutRequest {
128        self.common.requirements()
129    }
130}
131
132/// A single-block button that displays a boolean state derived from a
133/// [`listen::DynSource`] and can be clicked.
134#[derive(Clone)]
135pub struct ToggleButton<D> {
136    pub(in crate::vui::widgets::button) common: ButtonCommon<ToggleButtonVisualState>,
137    pub(in crate::vui::widgets::button) data_source: listen::DynSource<D>,
138    pub(in crate::vui::widgets::button) projection: Arc<dyn Fn(&D) -> bool + Send + Sync>,
139    pub(in crate::vui::widgets::button) action: Action,
140}
141
142impl<D: Clone + Sync + fmt::Debug> fmt::Debug for ToggleButton<D> {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        let Self {
145            common,
146            data_source,
147            projection,
148            action,
149        } = self;
150        f.debug_struct("ToggleButton")
151            .field("common", common)
152            .field("data_source", data_source)
153            .field("projection(data_source)", &projection(&data_source.get()))
154            .field("action", action)
155            .finish()
156    }
157}
158
159impl<D> ToggleButton<D> {
160    #[allow(missing_docs)]
161    pub fn new(
162        data_source: listen::DynSource<D>,
163        projection: impl Fn(&D) -> bool + Send + Sync + 'static,
164        label: impl Into<ButtonLabel>,
165        theme: &WidgetTheme,
166        action: impl Fn() + Send + Sync + 'static,
167    ) -> Arc<Self> {
168        Arc::new(Self {
169            common: ButtonCommon::new(
170                &theme.widget_blocks.subset(WidgetBlocks::ToggleButton),
171                label.into(),
172            ),
173            data_source,
174            projection: Arc::new(projection),
175            action: EphemeralOpaque::new(Arc::new(action)),
176        })
177    }
178}
179
180impl<D> vui::Layoutable for ToggleButton<D> {
181    fn requirements(&self) -> vui::LayoutRequest {
182        self.common.requirements()
183    }
184}
185
186impl universe::VisitHandles for ActionButton {
187    fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
188        let Self { common, action } = self;
189        common.visit_handles(visitor);
190        action.visit_handles(visitor);
191    }
192}
193impl<D> universe::VisitHandles for ToggleButton<D> {
194    fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
195        let Self {
196            common,
197            action,
198            data_source: _,
199            projection: _,
200        } = self;
201        common.visit_handles(visitor);
202        action.visit_handles(visitor);
203    }
204}
205
206// -------------------------------------------------------------------------------------------------
207
208/// Possible visual states of a button.
209///
210///
211/// [`ActionButton`] uses this directly, and other buttons' state may incorporate it.
212///
213/// The [`fmt::Display`] implementation of this type produces a string form suitable for
214/// naming blocks depicting this state; the [`Exhaust`] implementation allows iterating
215/// over all possible states.
216#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Exhaust)]
217#[non_exhaustive]
218pub struct ButtonVisualState {
219    // TODO: Add hover, disabled
220    /// The button looks pushed in.
221    pub(crate) pressed: bool,
222}
223
224/// Represents this value as a string suitable for naming blocks depicting this state.
225impl fmt::Display for ButtonVisualState {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        if self.pressed { "pressed" } else { "idle" }.fmt(f)
228    }
229}
230
231impl universe::VisitHandles for ButtonVisualState {
232    fn visit_handles(&self, _: &mut dyn universe::HandleVisitor) {}
233}
234
235/// Possible visual states of a [`ToggleButton`].
236///
237/// The [`fmt::Display`] implementation of this type produces a string form suitable for
238/// naming blocks depicting this state; the [`Exhaust`] implementation allows iterating
239/// over all possible states.
240#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Exhaust)]
241#[non_exhaustive]
242pub struct ToggleButtonVisualState {
243    pub(crate) common: ButtonVisualState,
244    /// The on/off value depicted.
245    pub value: bool,
246}
247
248/// Represents this value as a string suitable for naming blocks depicting this state.
249impl fmt::Display for ToggleButtonVisualState {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        let &Self { value, common } = self;
252        write!(
253            f,
254            "{value}-{common}",
255            value = match value {
256                false => "off",
257                true => "on",
258            },
259        )
260    }
261}
262
263impl ToggleButtonVisualState {
264    /// Returns the “off” (false) or “on” (true) state.
265    pub const fn new(value: bool) -> Self {
266        Self {
267            value,
268            common: ButtonVisualState { pressed: false },
269        }
270    }
271}
272
273impl universe::VisitHandles for ToggleButtonVisualState {
274    fn visit_handles(&self, _: &mut dyn universe::HandleVisitor) {}
275}