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.
325#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
326pub struct Padding {
327  /// Above and below the content.
328  pub vertical: u32,
329  /// Left and right of the content.
330  pub horizontal: u32,
331}
332
333/// How a widget looks. Every field is optional, and an unset field falls back
334/// to the widget's default: no background, black text in
335/// [`Font::DEFAULT`], no padding, square corners, no border, fully opaque.
336///
337/// [`Text`](crate::Text) takes `color` and `font` from the nearest widget
338/// that sets them, starting with itself and going up. Nothing else inherits.
339///
340/// `font`, `padding`, `clip`, `width` and `height` change the layout; the
341/// other fields only change how the widget is painted.
342///
343/// ```
344/// use crabshell_ui::{Color, Font, Style};
345///
346/// const BAR: Style = Style::new()
347///     .background(Color::rgb(0x1e, 0x1e, 0x2e))
348///     .color(Color::hex("#cdd6f4"))
349///     .font(Font::new("Inter", 13.0));
350/// const WS: Style = Style::new().padding(2, 8).radius(4.0);
351/// const WS_FOCUSED: Style = WS.background(Color::rgb(0x45, 0x47, 0x5a));
352/// ```
353#[derive(Clone, Copy, Debug, Default, PartialEq)]
354pub struct Style {
355  /// Fills the widget's rectangle.
356  pub background: Option<Color>,
357  /// Colors blending across the widget's rectangle, over the background.
358  pub gradient: Option<Gradient>,
359  /// A shadow under the widget's box.
360  pub shadow: Option<Shadow>,
361  /// The text color, inherited by [`Text`](crate::Text).
362  pub color: Option<Color>,
363  /// The text font, inherited by [`Text`](crate::Text).
364  pub font: Option<Font>,
365  /// Space between the edge and the content.
366  pub padding: Option<Padding>,
367  /// The corner radius in logical pixels.
368  pub radius: Option<f32>,
369  /// A border inside the edge.
370  pub border: Option<Border>,
371  /// From 0.0 (invisible) to 1.0 (opaque), applied to the widget and
372  /// everything inside it.
373  pub opacity: Option<f32>,
374  /// Whether the children are cut to the inside of the border, following
375  /// the rounded corners, like CSS's `overflow: hidden`.
376  pub clip: Option<bool>,
377  /// The width in logical pixels, instead of what the content needs.
378  pub width: Option<f32>,
379  /// The height in logical pixels, instead of what the content needs.
380  pub height: Option<f32>,
381  /// How a change to the paint fields fades in: background, gradient,
382  /// shadow, color, border, radius and opacity. Other fields change at
383  /// once.
384  pub transition: Option<Transition>,
385}
386
387impl Style {
388  /// A style that sets nothing.
389  pub const fn new() -> Style {
390    Style {
391      background: None,
392      gradient: None,
393      shadow: None,
394      color: None,
395      font: None,
396      padding: None,
397      radius: None,
398      border: None,
399      opacity: None,
400      clip: None,
401      width: None,
402      height: None,
403      transition: None,
404    }
405  }
406
407  /// Sets the background color.
408  pub const fn background(self, color: Color) -> Style {
409    Style {
410      background: Some(color),
411      ..self
412    }
413  }
414
415  /// Sets a gradient, drawn over the background.
416  pub const fn gradient(self, gradient: Gradient) -> Style {
417    Style {
418      gradient: Some(gradient),
419      ..self
420    }
421  }
422
423  /// Sets a shadow under the box.
424  pub const fn shadow(self, shadow: Shadow) -> Style {
425    Style {
426      shadow: Some(shadow),
427      ..self
428    }
429  }
430
431  /// Sets the text color.
432  pub const fn color(self, color: Color) -> Style {
433    Style {
434      color: Some(color),
435      ..self
436    }
437  }
438
439  /// Sets the text font.
440  pub const fn font(self, font: Font) -> Style {
441    Style {
442      font: Some(font),
443      ..self
444    }
445  }
446
447  /// Sets the padding: `vertical` above and below, `horizontal` left and
448  /// right, in logical pixels.
449  pub const fn padding(self, vertical: u32, horizontal: u32) -> Style {
450    Style {
451      padding: Some(Padding {
452        vertical,
453        horizontal,
454      }),
455      ..self
456    }
457  }
458
459  /// Sets the corner radius in logical pixels.
460  pub const fn radius(self, radius: f32) -> Style {
461    Style {
462      radius: Some(radius),
463      ..self
464    }
465  }
466
467  /// Sets a border of `width` logical pixels.
468  pub const fn border(self, width: f32, color: Color) -> Style {
469    Style {
470      border: Some(Border { width, color }),
471      ..self
472    }
473  }
474
475  /// Sets the opacity, from 0.0 to 1.0.
476  pub const fn opacity(self, opacity: f32) -> Style {
477    Style {
478      opacity: Some(opacity),
479      ..self
480    }
481  }
482
483  /// Sets whether the children are cut to the inside of the border,
484  /// following the rounded corners. Nothing is cut by default.
485  pub const fn clip(self, clip: bool) -> Style {
486    Style {
487      clip: Some(clip),
488      ..self
489    }
490  }
491
492  /// Sets the width in logical pixels, instead of what the content needs.
493  pub const fn width(self, width: f32) -> Style {
494    Style {
495      width: Some(width),
496      ..self
497    }
498  }
499
500  /// Sets the height in logical pixels, instead of what the content
501  /// needs.
502  pub const fn height(self, height: f32) -> Style {
503    Style {
504      height: Some(height),
505      ..self
506    }
507  }
508
509  /// Sets how a change to the paint fields fades in, as when a hover
510  /// style takes over: background, gradient, shadow, color, border, radius
511  /// and opacity move
512  /// over `transition`. The style being changed to says how.
513  pub const fn transition(self, transition: Transition) -> Style {
514    Style {
515      transition: Some(transition),
516      ..self
517    }
518  }
519
520  /// This style with every field that `over` sets replaced by its value.
521  /// Buttons use it to lay their hover and pressed styles over the normal
522  /// one.
523  pub const fn merge(self, over: Style) -> Style {
524    Style {
525      background: or(over.background, self.background),
526      gradient: or(over.gradient, self.gradient),
527      shadow: or(over.shadow, self.shadow),
528      color: or(over.color, self.color),
529      font: or(over.font, self.font),
530      padding: or(over.padding, self.padding),
531      radius: or(over.radius, self.radius),
532      border: or(over.border, self.border),
533      opacity: or(over.opacity, self.opacity),
534      clip: or(over.clip, self.clip),
535      width: or(over.width, self.width),
536      height: or(over.height, self.height),
537      transition: or(over.transition, self.transition),
538    }
539  }
540
541  /// Whether the children are cut to the widget's box.
542  pub fn clips(&self) -> bool {
543    self.clip == Some(true)
544  }
545
546  /// Whether the two styles lay out the same: same font, padding, size
547  /// and clipping, which changes how far the children's painting reaches.
548  pub(crate) fn same_layout(&self, other: &Style) -> bool {
549    self.font == other.font
550      && self.padding == other.padding
551      && self.width == other.width
552      && self.height == other.height
553      && self.clips() == other.clips()
554  }
555}
556
557/// `Option::or`, which isn't a `const fn` on stable Rust.
558const fn or<T: Copy>(first: Option<T>, second: Option<T>) -> Option<T> {
559  match first {
560    Some(value) => Some(value),
561    None => second,
562  }
563}
564
565impl Style {
566  /// The paint fields `t` of the way from `self` to `to`; every other
567  /// field is `to`'s. A field only one side sets fades from or to nothing:
568  /// a color from or to its transparent self, a border from or to 0 wide.
569  /// A text color only one sets changes at once, as the other comes
570  /// from an ancestor. Gradients of different shapes or numbers of colors
571  /// change at once.
572  pub(crate) fn lerp(&self, to: &Style, t: f32) -> Style {
573    let color = |from: Option<Color>, to: Option<Color>| match (from, to) {
574      (Some(from), Some(to)) => Some(from.lerp(&to, t)),
575      (None, Some(to)) => Some(Color::rgba(to.r, to.g, to.b, 0).lerp(&to, t)),
576      (Some(from), None) => Some(from.lerp(&Color::rgba(from.r, from.g, from.b, 0), t)),
577      (None, None) => None,
578    };
579    let border = match (self.border, to.border) {
580      (None, None) => None,
581      (from, to) => {
582        let from = from.or(to.map(|b| Border {
583          width: 0.0,
584          color: Color::rgba(b.color.r, b.color.g, b.color.b, 0),
585        }));
586        let to = to.or(from.map(|b| Border {
587          width: 0.0,
588          color: Color::rgba(b.color.r, b.color.g, b.color.b, 0),
589        }));
590        match (from, to) {
591          (Some(from), Some(to)) => Some(Border {
592            width: from.width.lerp(&to.width, t),
593            color: from.color.lerp(&to.color, t),
594          }),
595          _ => None,
596        }
597      }
598    };
599    let number = |from: Option<f32>, to: Option<f32>, none: f32| match (from, to) {
600      (None, None) => None,
601      (from, to) => Some(from.unwrap_or(none).lerp(&to.unwrap_or(none), t)),
602    };
603    let gradient = match (self.gradient, to.gradient) {
604      (Some(from), Some(end)) => from.lerp(&end, t).or(to.gradient),
605      (None, Some(end)) => end.clear().lerp(&end, t),
606      (Some(from), None) => from.lerp(&from.clear(), t),
607      (None, None) => None,
608    };
609    let shadow = match (self.shadow, to.shadow) {
610      (Some(from), Some(end)) => Some(from.lerp(&end, t)),
611      (None, Some(end)) => Some(end.clear().lerp(&end, t)),
612      (Some(from), None) => Some(from.lerp(&from.clear(), t)),
613      (None, None) => None,
614    };
615    Style {
616      background: color(self.background, to.background),
617      gradient,
618      shadow,
619      color: match (self.color, to.color) {
620        (Some(from), Some(to)) => Some(from.lerp(&to, t)),
621        _ => to.color,
622      },
623      border,
624      radius: number(self.radius, to.radius, 0.0),
625      opacity: number(self.opacity, to.opacity, 1.0),
626      ..*to
627    }
628  }
629}