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