Skip to main content

crabshell_ui/
style.rs

1//! Colors, fonts and [`Style`]. Everything here is `Copy` and built with
2//! `const fn`s, so a theme is a set of constants.
3
4use crate::animation::{Animate, Transition};
5use crate::geometry::Rect;
6
7/// An sRGB color with straight (not premultiplied) alpha.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub struct Color {
10  /// Red, 0 to 255.
11  pub r: u8,
12  /// Green, 0 to 255.
13  pub g: u8,
14  /// Blue, 0 to 255.
15  pub b: u8,
16  /// Alpha: 0 is transparent, 255 is opaque.
17  pub a: u8,
18}
19
20impl Color {
21  /// An opaque color.
22  pub const fn rgb(r: u8, g: u8, b: u8) -> Color {
23    Color { r, g, b, a: 255 }
24  }
25
26  /// A color with alpha: 0 is transparent, 255 is opaque.
27  pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
28    Color { r, g, b, a }
29  }
30
31  /// Parses `"#rrggbb"` or `"#rrggbbaa"`, in upper or lower case.
32  ///
33  /// # Panics
34  ///
35  /// If `hex` has any other form. In a `const`, that is a compile error:
36  ///
37  /// ```compile_fail
38  /// const BAD: crabshell_ui::Color = crabshell_ui::Color::hex("#12345");
39  /// ```
40  pub const fn hex(hex: &str) -> Color {
41    let bytes = hex.as_bytes();
42    if bytes.is_empty() || bytes[0] != b'#' || (bytes.len() != 7 && bytes.len() != 9) {
43      panic!("Color::hex takes \"#rrggbb\" or \"#rrggbbaa\"");
44    }
45    let a = if bytes.len() == 9 {
46      hex_byte(bytes[7], bytes[8])
47    } else {
48      255
49    };
50    Color {
51      r: hex_byte(bytes[1], bytes[2]),
52      g: hex_byte(bytes[3], bytes[4]),
53      b: hex_byte(bytes[5], bytes[6]),
54      a,
55    }
56  }
57}
58
59const fn hex_byte(high: u8, low: u8) -> u8 {
60  hex_digit(high) * 16 + hex_digit(low)
61}
62
63const fn hex_digit(digit: u8) -> u8 {
64  match digit {
65    b'0'..=b'9' => digit - b'0',
66    b'a'..=b'f' => digit - b'a' + 10,
67    b'A'..=b'F' => digit - b'A' + 10,
68    _ => panic!("Color::hex found a character that isn't a hex digit"),
69  }
70}
71
72/// A font: family, size and weight.
73///
74/// The family is resolved through fontconfig, so generic names such as
75/// `"sans-serif"` and `"monospace"` work too.
76#[derive(Clone, Copy, Debug, PartialEq)]
77pub struct Font {
78  /// The family name, such as `"Inter"` or `"sans-serif"`.
79  pub family: &'static str,
80  /// The size in logical pixels.
81  pub size: f32,
82  /// The weight, from 100 (thin) through 400 (regular) and 700 (bold) to
83  /// 900 (black).
84  pub weight: u16,
85}
86
87impl Font {
88  /// The font text uses when no ancestor sets one.
89  pub const DEFAULT: Font = Font::new("sans-serif", 13.0);
90
91  /// A regular-weight font of `family` at `size` logical pixels.
92  pub const fn new(family: &'static str, size: f32) -> Font {
93    Font {
94      family,
95      size,
96      weight: 400,
97    }
98  }
99
100  /// The same font with another weight, such as 700 for bold.
101  pub const fn weight(self, weight: u16) -> Font {
102    Font { weight, ..self }
103  }
104}
105
106/// A border drawn inside a widget's edge. It takes no layout space.
107#[derive(Clone, Copy, Debug, PartialEq)]
108pub struct Border {
109  /// The width in logical pixels.
110  pub width: f32,
111  /// The color.
112  pub color: Color,
113}
114
115/// Colors that blend into each other across a widget's box, drawn over
116/// its background with the same corners.
117///
118/// ```
119/// use crabshell_ui::{Color, Gradient, Style};
120///
121/// const DUSK: Style = Style::new().gradient(
122///     Gradient::linear(90.0, Color::hex("#1e1e2e"), Color::hex("#45475a"))
123///         .stop(0.5, Color::hex("#313244")),
124/// );
125/// ```
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct Gradient {
128  /// Which way the colors run.
129  pub shape: GradientShape,
130  /// The first `count` hold each color and where it sits, from 0 at the
131  /// start to 1 at the end.
132  stops: [(f32, Color); 4],
133  count: u8,
134}
135
136/// Which way a [`Gradient`]'s colors run.
137#[derive(Clone, Copy, Debug, PartialEq)]
138pub enum GradientShape {
139  /// Along a line through the middle at this many degrees, as in CSS: 0
140  /// runs from the bottom up, 90 from left to right, 180 from the top
141  /// down. Its ends meet the corners.
142  Linear(f32),
143  /// In circles out from the middle to the farthest corner.
144  Radial,
145}
146
147impl Gradient {
148  /// A gradient from `from` to `to` along a line at `angle` degrees: 0
149  /// runs from the bottom up, 90 from left to right.
150  pub const fn linear(angle: f32, from: Color, to: Color) -> Gradient {
151    Gradient::new(GradientShape::Linear(angle), from, to)
152  }
153
154  /// A gradient from `from` in the middle to `to` at the farthest corner.
155  pub const fn radial(from: Color, to: Color) -> Gradient {
156    Gradient::new(GradientShape::Radial, from, to)
157  }
158
159  const fn new(shape: GradientShape, from: Color, to: Color) -> Gradient {
160    Gradient {
161      shape,
162      stops: [(0.0, from), (1.0, to), (1.0, to), (1.0, to)],
163      count: 2,
164    }
165  }
166
167  /// Adds `color` at `at`, from 0 to 1, before the end color. Add them
168  /// in order. A gradient has at most four colors, so a third stop
169  /// panics.
170  pub const fn stop(self, at: f32, color: Color) -> Gradient {
171    assert!(self.count < 4, "a gradient has at most four colors");
172    let mut stops = self.stops;
173    let last = self.count as usize - 1;
174    stops[last + 1] = stops[last];
175    stops[last] = (at, color);
176    Gradient {
177      stops,
178      count: self.count + 1,
179      ..self
180    }
181  }
182
183  /// Each color and where it sits, from the start to the end.
184  pub fn stops(&self) -> &[(f32, Color)] {
185    &self.stops[..usize::from(self.count)]
186  }
187
188  /// `t` of the way from this gradient to `to`, or `None` when their
189  /// shapes or numbers of colors differ.
190  fn lerp(&self, to: &Gradient, t: f32) -> Option<Gradient> {
191    let shape = match (self.shape, to.shape) {
192      (GradientShape::Linear(from), GradientShape::Linear(to)) => {
193        GradientShape::Linear(from.lerp(&to, t))
194      }
195      (GradientShape::Radial, GradientShape::Radial) => GradientShape::Radial,
196      _ => return None,
197    };
198    if self.count != to.count {
199      return None;
200    }
201    let mut stops = to.stops;
202    for (stop, (from, to)) in stops.iter_mut().zip(self.stops.iter().zip(to.stops)) {
203      *stop = (from.0.lerp(&to.0, t), from.1.lerp(&to.1, t));
204    }
205    Some(Gradient {
206      shape,
207      stops,
208      count: to.count,
209    })
210  }
211
212  /// This gradient with every color transparent.
213  fn clear(mut self) -> Gradient {
214    for (_, color) in &mut self.stops {
215      color.a = 0;
216    }
217    self
218  }
219}
220
221/// A shadow cast by a widget's box, as CSS's `box-shadow`: the box's
222/// shape moved by `x` and `y`, grown by `spread` and blurred over `blur`
223/// logical pixels. Unlike CSS's, it is drawn under the box too, so a
224/// translucent background shows it through, unless it is
225/// [`cut_out`](Shadow::cut_out).
226///
227/// ```
228/// use crabshell_ui::{Color, Shadow, Style};
229///
230/// const CARD: Style = Style::new()
231///     .background(Color::hex("#1e1e2e"))
232///     .radius(8.0)
233///     .shadow(Shadow::new(0.0, 4.0, 12.0, Color::rgba(0, 0, 0, 96)));
234/// ```
235#[derive(Clone, Copy, Debug, PartialEq)]
236pub struct Shadow {
237  /// How far right it is moved.
238  pub x: f32,
239  /// How far down it is moved.
240  pub y: f32,
241  /// How far its edge is blurred. 0 is a sharp edge.
242  pub blur: f32,
243  /// How much larger than the box it is on each side, or smaller when
244  /// negative.
245  pub spread: f32,
246  /// Its color.
247  pub color: Color,
248  /// Whether it is left out under the box, as under translucent glass.
249  pub cut_out: bool,
250}
251
252impl Shadow {
253  /// A shadow moved by `x` and `y` and blurred over `blur`, the box's
254  /// size.
255  pub const fn new(x: f32, y: f32, blur: f32, color: Color) -> Shadow {
256    Shadow {
257      x,
258      y,
259      blur,
260      spread: 0.0,
261      color,
262      cut_out: false,
263    }
264  }
265
266  /// The same shadow `spread` larger on each side.
267  pub const fn spread(self, spread: f32) -> Shadow {
268    Shadow { spread, ..self }
269  }
270
271  /// The same shadow left out under the box's own shape, so a translucent
272  /// background, such as glass the compositor blurs, shows what is
273  /// behind it instead of the shadow. CSS's `box-shadow` is always drawn
274  /// this way.
275  pub const fn cut_out(self) -> Shadow {
276    Shadow {
277      cut_out: true,
278      ..self
279    }
280  }
281
282  /// The area this shadow of `rect` covers: its shape, grown by one and a
283  /// half blurs, past which the blur fades below a 255th. A spread that
284  /// swallows the box still reaches between the edges it would have, so
285  /// a fade from it stays within its start and end. A shadow with a
286  /// number that isn't finite is never drawn, so it reaches only `rect`.
287  pub(crate) fn reach(&self, rect: Rect) -> Rect {
288    let grow = self.spread + 1.5 * self.blur.max(0.0);
289    if ![self.x, self.y, grow].iter().all(|n| n.is_finite()) {
290      return rect;
291    }
292    let (left, right) = (rect.x + self.x - grow, rect.right() + self.x + grow);
293    let (top, bottom) = (rect.y + self.y - grow, rect.bottom() + self.y + grow);
294    Rect::new(
295      left.min(right),
296      top.min(bottom),
297      (right - left).abs(),
298      (bottom - top).abs(),
299    )
300  }
301
302  /// `t` of the way to `to`.
303  fn lerp(&self, to: &Shadow, t: f32) -> Shadow {
304    Shadow {
305      x: self.x.lerp(&to.x, t),
306      y: self.y.lerp(&to.y, t),
307      blur: self.blur.lerp(&to.blur, t),
308      spread: self.spread.lerp(&to.spread, t),
309      color: self.color.lerp(&to.color, t),
310      // Not a number: it changes at once.
311      cut_out: to.cut_out,
312    }
313  }
314
315  /// This shadow, transparent.
316  fn clear(self) -> Shadow {
317    Shadow {
318      color: Color::rgba(self.color.r, self.color.g, self.color.b, 0),
319      ..self
320    }
321  }
322}
323
324/// Space between a widget's edge and its content, in logical pixels, on
325/// each side.
326#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
327pub struct Padding {
328  /// Above the content.
329  pub top: u32,
330  /// Right of the content.
331  pub right: u32,
332  /// Below the content.
333  pub bottom: u32,
334  /// Left of the content.
335  pub left: u32,
336}
337
338/// How a widget looks. Every field is optional, and an unset field falls back
339/// to the widget's default: no background, black text in
340/// [`Font::DEFAULT`], no padding, square corners, no border, fully opaque.
341///
342/// [`Text`](crate::Text) takes `color` and `font` from the nearest widget
343/// that sets them, starting with itself and going up. Nothing else inherits.
344///
345/// `font`, `padding`, `clip`, `width` and `height` change the layout; the
346/// other fields only change how the widget is painted.
347///
348/// ```
349/// use crabshell_ui::{Color, Font, Style};
350///
351/// const BAR: Style = Style::new()
352///     .background(Color::rgb(0x1e, 0x1e, 0x2e))
353///     .color(Color::hex("#cdd6f4"))
354///     .font(Font::new("Inter", 13.0));
355/// const WS: Style = Style::new().padding(2, 8).radius(4.0);
356/// const WS_FOCUSED: Style = WS.background(Color::rgb(0x45, 0x47, 0x5a));
357/// ```
358#[derive(Clone, Copy, Debug, Default, PartialEq)]
359pub struct Style {
360  /// Fills the widget's rectangle.
361  pub background: Option<Color>,
362  /// Colors blending across the widget's rectangle, over the background.
363  pub gradient: Option<Gradient>,
364  /// A shadow under the widget's box.
365  pub shadow: Option<Shadow>,
366  /// The text color, inherited by [`Text`](crate::Text).
367  pub color: Option<Color>,
368  /// The text font, inherited by [`Text`](crate::Text).
369  pub font: Option<Font>,
370  /// Space between the edge and the content.
371  pub padding: Option<Padding>,
372  /// The corner radius in logical pixels.
373  pub radius: Option<f32>,
374  /// A border inside the edge.
375  pub border: Option<Border>,
376  /// From 0.0 (invisible) to 1.0 (opaque), applied to the widget and
377  /// everything inside it.
378  pub opacity: Option<f32>,
379  /// Whether the children are cut to the inside of the border, following
380  /// the rounded corners, like CSS's `overflow: hidden`.
381  pub clip: Option<bool>,
382  /// The width in logical pixels, instead of what the content needs.
383  pub width: Option<f32>,
384  /// The height in logical pixels, instead of what the content needs.
385  pub height: Option<f32>,
386  /// How a change to the paint fields fades in: background, gradient,
387  /// shadow, color, border, radius and opacity. Other fields change at
388  /// once.
389  pub transition: Option<Transition>,
390}
391
392impl Style {
393  /// A style that sets nothing.
394  pub const fn new() -> Style {
395    Style {
396      background: None,
397      gradient: None,
398      shadow: None,
399      color: None,
400      font: None,
401      padding: None,
402      radius: None,
403      border: None,
404      opacity: None,
405      clip: None,
406      width: None,
407      height: None,
408      transition: None,
409    }
410  }
411
412  /// Sets the background color.
413  pub const fn background(self, color: Color) -> Style {
414    Style {
415      background: Some(color),
416      ..self
417    }
418  }
419
420  /// Sets a gradient, drawn over the background.
421  pub const fn gradient(self, gradient: Gradient) -> Style {
422    Style {
423      gradient: Some(gradient),
424      ..self
425    }
426  }
427
428  /// Sets a shadow under the box.
429  pub const fn shadow(self, shadow: Shadow) -> Style {
430    Style {
431      shadow: Some(shadow),
432      ..self
433    }
434  }
435
436  /// Sets the text color.
437  pub const fn color(self, color: Color) -> Style {
438    Style {
439      color: Some(color),
440      ..self
441    }
442  }
443
444  /// Sets the text font.
445  pub const fn font(self, font: Font) -> Style {
446    Style {
447      font: Some(font),
448      ..self
449    }
450  }
451
452  /// Sets the padding: `vertical` above and below, `horizontal` left and
453  /// right, in logical pixels.
454  pub const fn padding(self, vertical: u32, horizontal: u32) -> Style {
455    Style {
456      padding: Some(Padding {
457        top: vertical,
458        right: horizontal,
459        bottom: vertical,
460        left: horizontal,
461      }),
462      ..self
463    }
464  }
465
466  /// Sets the padding above the content, in logical pixels, keeping the
467  /// other sides this style sets, or 0 where it sets none. As with every
468  /// field, [`merge`](Self::merge) takes the whole padding of the style on
469  /// top, so a hover style built from `Style::new()` sets the rest to 0.
470  ///
471  /// ```
472  /// use crabshell_ui::Style;
473  ///
474  /// const PILL: Style = Style::new().padding(2, 8).padding_left(12);
475  /// ```
476  pub const fn padding_top(self, top: u32) -> Style {
477    let mut padding = self.sides();
478    padding.top = top;
479    Style {
480      padding: Some(padding),
481      ..self
482    }
483  }
484
485  /// Sets the padding right of the content, keeping the other sides, as
486  /// [`padding_top`](Self::padding_top) does.
487  pub const fn padding_right(self, right: u32) -> Style {
488    let mut padding = self.sides();
489    padding.right = right;
490    Style {
491      padding: Some(padding),
492      ..self
493    }
494  }
495
496  /// Sets the padding below the content, keeping the other sides, as
497  /// [`padding_top`](Self::padding_top) does.
498  pub const fn padding_bottom(self, bottom: u32) -> Style {
499    let mut padding = self.sides();
500    padding.bottom = bottom;
501    Style {
502      padding: Some(padding),
503      ..self
504    }
505  }
506
507  /// Sets the padding left of the content, keeping the other sides, as
508  /// [`padding_top`](Self::padding_top) does.
509  pub const fn padding_left(self, left: u32) -> Style {
510    let mut padding = self.sides();
511    padding.left = left;
512    Style {
513      padding: Some(padding),
514      ..self
515    }
516  }
517
518  /// The padding this style sets, or 0 on every side.
519  const fn sides(self) -> Padding {
520    match self.padding {
521      Some(padding) => padding,
522      None => Padding {
523        top: 0,
524        right: 0,
525        bottom: 0,
526        left: 0,
527      },
528    }
529  }
530
531  /// Sets the corner radius in logical pixels.
532  pub const fn radius(self, radius: f32) -> Style {
533    Style {
534      radius: Some(radius),
535      ..self
536    }
537  }
538
539  /// Sets a border of `width` logical pixels.
540  pub const fn border(self, width: f32, color: Color) -> Style {
541    Style {
542      border: Some(Border { width, color }),
543      ..self
544    }
545  }
546
547  /// Sets the opacity, from 0.0 to 1.0.
548  pub const fn opacity(self, opacity: f32) -> Style {
549    Style {
550      opacity: Some(opacity),
551      ..self
552    }
553  }
554
555  /// Sets whether the children are cut to the inside of the border,
556  /// following the rounded corners. Nothing is cut by default.
557  pub const fn clip(self, clip: bool) -> Style {
558    Style {
559      clip: Some(clip),
560      ..self
561    }
562  }
563
564  /// Sets the width in logical pixels, instead of what the content needs.
565  pub const fn width(self, width: f32) -> Style {
566    Style {
567      width: Some(width),
568      ..self
569    }
570  }
571
572  /// Sets the height in logical pixels, instead of what the content
573  /// needs.
574  pub const fn height(self, height: f32) -> Style {
575    Style {
576      height: Some(height),
577      ..self
578    }
579  }
580
581  /// Sets how a change to the paint fields fades in, as when a hover
582  /// style takes over: background, gradient, shadow, color, border, radius
583  /// and opacity move
584  /// over `transition`. The style being changed to says how.
585  pub const fn transition(self, transition: Transition) -> Style {
586    Style {
587      transition: Some(transition),
588      ..self
589    }
590  }
591
592  /// This style with every field that `over` sets replaced by its value.
593  /// Buttons use it to lay their hover and pressed styles over the normal
594  /// one.
595  pub const fn merge(self, over: Style) -> Style {
596    Style {
597      background: or(over.background, self.background),
598      gradient: or(over.gradient, self.gradient),
599      shadow: or(over.shadow, self.shadow),
600      color: or(over.color, self.color),
601      font: or(over.font, self.font),
602      padding: or(over.padding, self.padding),
603      radius: or(over.radius, self.radius),
604      border: or(over.border, self.border),
605      opacity: or(over.opacity, self.opacity),
606      clip: or(over.clip, self.clip),
607      width: or(over.width, self.width),
608      height: or(over.height, self.height),
609      transition: or(over.transition, self.transition),
610    }
611  }
612
613  /// Whether the children are cut to the widget's box.
614  pub fn clips(&self) -> bool {
615    self.clip == Some(true)
616  }
617
618  /// Whether the two styles lay out the same: same font, padding, size
619  /// and clipping, which changes how far the children's painting reaches.
620  pub(crate) fn same_layout(&self, other: &Style) -> bool {
621    self.font == other.font
622      && self.padding == other.padding
623      && self.width == other.width
624      && self.height == other.height
625      && self.clips() == other.clips()
626  }
627}
628
629/// `Option::or`, which isn't a `const fn` on stable Rust.
630const fn or<T: Copy>(first: Option<T>, second: Option<T>) -> Option<T> {
631  match first {
632    Some(value) => Some(value),
633    None => second,
634  }
635}
636
637impl Style {
638  /// The paint fields `t` of the way from `self` to `to`; every other
639  /// field is `to`'s. A field only one side sets fades from or to nothing:
640  /// a color from or to its transparent self, a border from or to 0 wide.
641  /// A text color only one sets changes at once, as the other comes
642  /// from an ancestor. Gradients of different shapes or numbers of colors
643  /// change at once.
644  pub(crate) fn lerp(&self, to: &Style, t: f32) -> Style {
645    let color = |from: Option<Color>, to: Option<Color>| match (from, to) {
646      (Some(from), Some(to)) => Some(from.lerp(&to, t)),
647      (None, Some(to)) => Some(Color::rgba(to.r, to.g, to.b, 0).lerp(&to, t)),
648      (Some(from), None) => Some(from.lerp(&Color::rgba(from.r, from.g, from.b, 0), t)),
649      (None, None) => None,
650    };
651    let border = match (self.border, to.border) {
652      (None, None) => None,
653      (from, to) => {
654        let from = from.or(to.map(|b| Border {
655          width: 0.0,
656          color: Color::rgba(b.color.r, b.color.g, b.color.b, 0),
657        }));
658        let to = to.or(from.map(|b| Border {
659          width: 0.0,
660          color: Color::rgba(b.color.r, b.color.g, b.color.b, 0),
661        }));
662        match (from, to) {
663          (Some(from), Some(to)) => Some(Border {
664            width: from.width.lerp(&to.width, t),
665            color: from.color.lerp(&to.color, t),
666          }),
667          _ => None,
668        }
669      }
670    };
671    let number = |from: Option<f32>, to: Option<f32>, none: f32| match (from, to) {
672      (None, None) => None,
673      (from, to) => Some(from.unwrap_or(none).lerp(&to.unwrap_or(none), t)),
674    };
675    let gradient = match (self.gradient, to.gradient) {
676      (Some(from), Some(end)) => from.lerp(&end, t).or(to.gradient),
677      (None, Some(end)) => end.clear().lerp(&end, t),
678      (Some(from), None) => from.lerp(&from.clear(), t),
679      (None, None) => None,
680    };
681    let shadow = match (self.shadow, to.shadow) {
682      (Some(from), Some(end)) => Some(from.lerp(&end, t)),
683      (None, Some(end)) => Some(end.clear().lerp(&end, t)),
684      (Some(from), None) => Some(from.lerp(&from.clear(), t)),
685      (None, None) => None,
686    };
687    Style {
688      background: color(self.background, to.background),
689      gradient,
690      shadow,
691      color: match (self.color, to.color) {
692        (Some(from), Some(to)) => Some(from.lerp(&to, t)),
693        _ => to.color,
694      },
695      border,
696      radius: number(self.radius, to.radius, 0.0),
697      opacity: number(self.opacity, to.opacity, 1.0),
698      ..*to
699    }
700  }
701}