Skip to main content

crabshell_ui/
widgets.rs

1//! The widgets, and the [`Widget`] trait they share.
2//!
3//! A widget value is only a description. Nothing happens until its window is
4//! created: then each description becomes a node in the window's tree, and
5//! each closure property becomes an effect that updates that node.
6
7use std::hash::Hash;
8use std::path::PathBuf;
9use std::rc::Rc;
10
11use crabshell_reactive::Memo;
12
13use crate::cx::Cx;
14use crate::input::{Handler, Handlers};
15use crate::keyed::{self, KeyedMount};
16use crate::prop::{IntoProp, Prop};
17use crate::style::Style;
18
19/// Something that can be placed in a window: one of this crate's widgets, or
20/// a type of your own that is made of them.
21///
22/// To make your own, return another widget's element. Calling
23/// `into_element` on a container describes everything inside it too, so the
24/// result is plain data:
25///
26/// ```
27/// use crabshell_ui::{Element, Row, Text, Widget};
28///
29/// struct Greeting;
30///
31/// impl Widget for Greeting {
32///     fn into_element(self: Box<Self>) -> Element {
33///         Box::new(Row::new().gap(4).child(Text::new("hello")).child(Text::new("world")))
34///             .into_element()
35///     }
36/// }
37/// ```
38pub trait Widget: 'static {
39  /// Turns the description, and everything inside it, into the element
40  /// the window builds nodes from. It runs when the window is created or a
41  /// list item first appears, before the window's tree is touched and
42  /// without recording what it reads, so it only describes.
43  fn into_element(self: Box<Self>) -> Element;
44}
45
46/// What a [`Widget`] turns into: plain data, with every child already turned
47/// into its element. Only this crate's widgets create one.
48pub struct Element {
49  pub(crate) name: &'static str,
50  pub(crate) style: Option<Prop<Style>>,
51  pub(crate) kind: ElementKind,
52  pub(crate) children: Vec<ElementChild>,
53}
54
55pub(crate) enum ElementKind {
56  Flex(FlexProps),
57  Stack,
58  Spacer,
59  Text(TextProps),
60  Icon(IconProps),
61  Button(ButtonProps),
62}
63
64/// One entry of a container's child list: a single widget, or a keyed list
65/// that grows and shrinks.
66pub(crate) enum Child {
67  One(Box<dyn Widget>),
68  Keyed(KeyedMount),
69}
70
71/// A [`Child`] with its widget turned into an element.
72pub(crate) enum ElementChild {
73  One(Box<Element>),
74  Keyed(KeyedMount),
75}
76
77/// Turns every child widget into its element, which does the same for its
78/// own children.
79fn describe(children: Vec<Child>) -> Vec<ElementChild> {
80  children
81    .into_iter()
82    .map(|child| match child {
83      Child::One(widget) => ElementChild::One(Box::new(widget.into_element())),
84      Child::Keyed(mount) => ElementChild::Keyed(mount),
85    })
86    .collect()
87}
88
89pub(crate) struct FlexProps {
90  pub(crate) column: bool,
91  /// Takes the room left in its parent, like a spacer. Only `Menu` sets
92  /// it, so an entry's row fills its button.
93  pub(crate) grow: bool,
94  pub(crate) gap: Option<Prop<u32>>,
95  pub(crate) padding_vertical: Option<Prop<u32>>,
96  pub(crate) padding_horizontal: Option<Prop<u32>>,
97  pub(crate) align: Option<Prop<Align>>,
98  pub(crate) justify: Option<Prop<Justify>>,
99}
100
101pub(crate) struct TextProps {
102  pub(crate) text: Prop<String>,
103  pub(crate) ellipsis: Option<Prop<bool>>,
104  pub(crate) max_lines: Option<Prop<u32>>,
105}
106
107pub(crate) struct IconProps {
108  pub(crate) source: Prop<IconSource>,
109  pub(crate) size: Option<Prop<u32>>,
110}
111
112pub(crate) struct ButtonProps {
113  pub(crate) hover_style: Option<Prop<Style>>,
114  pub(crate) pressed_style: Option<Prop<Style>>,
115  pub(crate) handlers: Handlers,
116}
117
118/// How a [`Row`] or [`Column`] places children across its direction: up and
119/// down in a row, left and right in a column.
120#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
121pub enum Align {
122  /// At the start: the top of a row, the left of a column.
123  Start,
124  /// In the middle.
125  Center,
126  /// At the end: the bottom of a row, the right of a column.
127  End,
128  /// Stretched to fill. The default, as in CSS.
129  #[default]
130  Stretch,
131}
132
133/// How a [`Row`] or [`Column`] places children along its direction when
134/// they don't fill it.
135#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
136pub enum Justify {
137  /// Packed at the start. The default.
138  #[default]
139  Start,
140  /// Packed in the middle.
141  Center,
142  /// Packed at the end.
143  End,
144  /// The first child at the start, the last at the end, equal space between.
145  SpaceBetween,
146  /// Equal space around each child.
147  SpaceAround,
148  /// Equal space between children and at both ends.
149  SpaceEvenly,
150}
151
152/// What an [`Icon`] shows.
153#[derive(Clone, Debug, PartialEq)]
154pub enum IconSource {
155  /// A freedesktop icon name such as `"audio-volume-high"`, looked up in
156  /// the icon theme.
157  Name(String),
158  /// An SVG or PNG file.
159  File(PathBuf),
160  /// Raw pixels, as a tray item sends them.
161  Pixels(IconPixels),
162}
163
164/// Raw icon pixels: ARGB32 in network byte order (A, R, G, B bytes), row
165/// by row, as the StatusNotifierItem spec sends them.
166#[derive(Clone, Debug, PartialEq)]
167pub struct IconPixels {
168  /// Width in pixels.
169  pub width: u32,
170  /// Height in pixels.
171  pub height: u32,
172  /// `width * height * 4` bytes. Shared, so the same icon isn't copied.
173  pub data: Rc<[u8]>,
174}
175
176/// The methods every container has: `child` and `children_keyed`.
177macro_rules! container_methods {
178  () => {
179    /// Adds `child` after the children added so far.
180    pub fn child(mut self, child: impl Widget) -> Self {
181      self.children.push(Child::One(Box::new(child)));
182      self
183    }
184
185    /// Adds one child per item, after the children added so far.
186    ///
187    /// `items` runs in an effect, so the list updates when a signal it
188    /// read changes. `key` names each item. `view` builds the child for
189    /// a key the first time it appears, and that child is kept while
190    /// its key stays in the list, even if it moves. `view` receives the
191    /// item's [`Cx`] and a [`Memo`] that updates when that item's value
192    /// changes. When a key leaves the list, its child and everything
193    /// created in `view` are freed. If two items share a key, only the
194    /// first is shown and the duplicate is logged.
195    pub fn children_keyed<T, K, W>(
196      mut self,
197      items: impl FnMut() -> Vec<T> + 'static,
198      key: impl FnMut(&T) -> K + 'static,
199      view: impl FnMut(&Cx, Memo<T>) -> W + 'static,
200    ) -> Self
201    where
202      T: Clone + PartialEq + 'static,
203      K: Eq + Hash + 'static,
204      W: Widget,
205    {
206      self
207        .children
208        .push(Child::Keyed(keyed::mount(items, key, view)));
209      self
210    }
211  };
212}
213
214/// The `style` method every widget has.
215macro_rules! style_method {
216  () => {
217    /// Sets the widget's [`Style`].
218    pub fn style(mut self, style: impl IntoProp<Style>) -> Self {
219      self.style = Some(style.into_prop());
220      self
221    }
222  };
223}
224
225/// Defines `Row` and `Column`, which differ only in direction.
226macro_rules! flex_widget {
227  ($name:ident, $column:literal, $doc:literal) => {
228    #[doc = $doc]
229    ///
230    /// `gap`, `padding`, `align` and `justify` change the layout. Each
231    /// takes a value or a closure.
232    pub struct $name {
233      flex: FlexProps,
234      style: Option<Prop<Style>>,
235      children: Vec<Child>,
236    }
237
238    impl $name {
239      /// An empty container.
240      pub fn new() -> Self {
241        $name {
242          flex: FlexProps {
243            column: $column,
244            grow: false,
245            gap: None,
246            padding_vertical: None,
247            padding_horizontal: None,
248            align: None,
249            justify: None,
250          },
251          style: None,
252          children: Vec::new(),
253        }
254      }
255
256      /// Sets the space between children in logical pixels.
257      pub fn gap(mut self, gap: impl IntoProp<u32>) -> Self {
258        self.flex.gap = Some(gap.into_prop());
259        self
260      }
261
262      /// Sets the padding: `vertical` above and below, `horizontal`
263      /// left and right, in logical pixels. It wins over the padding
264      /// of the container's [`Style`].
265      pub fn padding(
266        mut self,
267        vertical: impl IntoProp<u32>,
268        horizontal: impl IntoProp<u32>,
269      ) -> Self {
270        self.flex.padding_vertical = Some(vertical.into_prop());
271        self.flex.padding_horizontal = Some(horizontal.into_prop());
272        self
273      }
274
275      /// Sets how children are placed across the direction.
276      pub fn align(mut self, align: impl IntoProp<Align>) -> Self {
277        self.flex.align = Some(align.into_prop());
278        self
279      }
280
281      /// Sets how children are placed along the direction.
282      pub fn justify(mut self, justify: impl IntoProp<Justify>) -> Self {
283        self.flex.justify = Some(justify.into_prop());
284        self
285      }
286
287      container_methods!();
288      style_method!();
289    }
290
291    impl Default for $name {
292      fn default() -> Self {
293        Self::new()
294      }
295    }
296
297    impl Widget for $name {
298      fn into_element(self: Box<Self>) -> Element {
299        Element {
300          name: stringify!($name),
301          style: self.style,
302          kind: ElementKind::Flex(self.flex),
303          children: describe(self.children),
304        }
305      }
306    }
307  };
308}
309
310flex_widget!(
311  Row,
312  false,
313  "Lays its children out left to right, like a CSS flexbox row."
314);
315flex_widget!(
316  Column,
317  true,
318  "Lays its children out top to bottom, like a CSS flexbox column."
319);
320
321/// Draws its children on top of each other in one cell, each stretched to
322/// the size of the largest. Later children are on top.
323pub struct Stack {
324  style: Option<Prop<Style>>,
325  children: Vec<Child>,
326}
327
328impl Stack {
329  /// An empty stack.
330  pub fn new() -> Stack {
331    Stack {
332      style: None,
333      children: Vec::new(),
334    }
335  }
336
337  container_methods!();
338  style_method!();
339}
340
341impl Default for Stack {
342  fn default() -> Self {
343    Self::new()
344  }
345}
346
347impl Widget for Stack {
348  fn into_element(self: Box<Self>) -> Element {
349    Element {
350      name: "Stack",
351      style: self.style,
352      kind: ElementKind::Stack,
353      children: describe(self.children),
354    }
355  }
356}
357
358/// Takes the space left over in a [`Row`] or [`Column`]. Several spacers
359/// share it equally.
360pub struct Spacer;
361
362impl Spacer {
363  /// A spacer.
364  pub fn new() -> Spacer {
365    Spacer
366  }
367}
368
369impl Default for Spacer {
370  fn default() -> Self {
371    Self::new()
372  }
373}
374
375impl Widget for Spacer {
376  fn into_element(self: Box<Self>) -> Element {
377    Element {
378      name: "Spacer",
379      style: None,
380      kind: ElementKind::Spacer,
381      children: Vec::new(),
382    }
383  }
384}
385
386/// A line of text.
387///
388/// The text, `ellipsis` and `max_lines` change the layout. Color and font
389/// come from the nearest widget whose [`Style`] sets them, starting with the
390/// text's own.
391pub struct Text {
392  text: TextProps,
393  style: Option<Prop<Style>>,
394}
395
396impl Text {
397  /// Text showing `text`: a string, or a closure that returns a `String`.
398  pub fn new(text: impl IntoProp<String>) -> Text {
399    Text {
400      text: TextProps {
401        text: text.into_prop(),
402        ellipsis: None,
403        max_lines: None,
404      },
405      style: None,
406    }
407  }
408
409  /// When on, text that doesn't fit ends in "…" instead of being cut off.
410  /// Off by default.
411  pub fn ellipsis(mut self, ellipsis: impl IntoProp<bool>) -> Text {
412    self.text.ellipsis = Some(ellipsis.into_prop());
413    self
414  }
415
416  /// Lets the text wrap onto at most this many lines, at word boundaries,
417  /// or inside a word too long for a line. The default is 1, which never
418  /// wraps. 0 counts as 1.
419  pub fn max_lines(mut self, max_lines: impl IntoProp<u32>) -> Text {
420    self.text.max_lines = Some(max_lines.into_prop());
421    self
422  }
423
424  style_method!();
425}
426
427impl Widget for Text {
428  fn into_element(self: Box<Self>) -> Element {
429    Element {
430      name: "Text",
431      style: self.style,
432      kind: ElementKind::Text(self.text),
433      children: Vec::new(),
434    }
435  }
436}
437
438/// An icon, drawn as a square.
439pub struct Icon {
440  icon: IconProps,
441  style: Option<Prop<Style>>,
442}
443
444impl Icon {
445  /// The default size in logical pixels.
446  pub const DEFAULT_SIZE: u32 = 16;
447
448  /// An icon showing `source`: an icon name (`&str`), a file (`&Path` or
449  /// `PathBuf`), an [`IconSource`], or a closure that returns one.
450  pub fn new(source: impl IntoProp<IconSource>) -> Icon {
451    Icon {
452      icon: IconProps {
453        source: source.into_prop(),
454        size: None,
455      },
456      style: None,
457    }
458  }
459
460  /// Sets the width and height in logical pixels.
461  pub fn size(mut self, size: impl IntoProp<u32>) -> Icon {
462    self.icon.size = Some(size.into_prop());
463    self
464  }
465
466  style_method!();
467}
468
469impl Widget for Icon {
470  fn into_element(self: Box<Self>) -> Element {
471    Element {
472      name: "Icon",
473      style: self.style,
474      kind: ElementKind::Icon(self.icon),
475      children: Vec::new(),
476    }
477  }
478}
479
480/// A clickable box around one child, which it centers.
481///
482/// While the pointer is over it, `hover_style` is laid over its style, and
483/// while it is also held down, `pressed_style` on top of that. Handlers run
484/// on the main thread and receive the button's [`Cx`]. A handler that panics
485/// is logged and never called again.
486pub struct Button {
487  button: ButtonProps,
488  style: Option<Prop<Style>>,
489  child: Box<dyn Widget>,
490}
491
492impl Button {
493  /// A button around `child`.
494  pub fn new(child: impl Widget) -> Button {
495    Button {
496      child: Box::new(child),
497      button: ButtonProps {
498        hover_style: None,
499        pressed_style: None,
500        handlers: Handlers::default(),
501      },
502      style: None,
503    }
504  }
505
506  /// Sets the style laid over the normal one while the pointer is over the
507  /// button.
508  pub fn hover_style(mut self, style: impl IntoProp<Style>) -> Button {
509    self.button.hover_style = Some(style.into_prop());
510    self
511  }
512
513  /// Sets the style laid over the hover style while the button is held
514  /// down.
515  pub fn pressed_style(mut self, style: impl IntoProp<Style>) -> Button {
516    self.button.pressed_style = Some(style.into_prop());
517    self
518  }
519
520  /// Runs `handler` when the left button is pressed and released over this
521  /// button.
522  pub fn on_click(mut self, handler: impl Fn(&Cx) + 'static) -> Button {
523    self.button.handlers.click = Some(Rc::new(Handler::new(handler)));
524    self
525  }
526
527  /// Runs `handler` when the right button is pressed and released over this
528  /// button.
529  pub fn on_right_click(mut self, handler: impl Fn(&Cx) + 'static) -> Button {
530    self.button.handlers.right_click = Some(Rc::new(Handler::new(handler)));
531    self
532  }
533
534  /// Runs `handler` when the middle button is pressed and released over
535  /// this button.
536  pub fn on_middle_click(mut self, handler: impl Fn(&Cx) + 'static) -> Button {
537    self.button.handlers.middle_click = Some(Rc::new(Handler::new(handler)));
538    self
539  }
540
541  /// Runs `handler` with whole scroll steps while the pointer is over this
542  /// button: positive for up, negative for down. A mouse wheel gives one
543  /// step per notch; touchpad scrolling gives one step per 15 logical
544  /// pixels.
545  pub fn on_scroll(mut self, handler: impl Fn(&Cx, i32) + 'static) -> Button {
546    self.button.handlers.scroll = Some(Rc::new(Handler::new(handler)));
547    self
548  }
549
550  style_method!();
551}
552
553impl Widget for Button {
554  fn into_element(self: Box<Self>) -> Element {
555    Element {
556      name: "Button",
557      style: self.style,
558      kind: ElementKind::Button(self.button),
559      children: vec![ElementChild::One(Box::new(self.child.into_element()))],
560    }
561  }
562}