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