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
4/// An sRGB color with straight (not premultiplied) alpha.
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub struct Color {
7  /// Red, 0 to 255.
8  pub r: u8,
9  /// Green, 0 to 255.
10  pub g: u8,
11  /// Blue, 0 to 255.
12  pub b: u8,
13  /// Alpha: 0 is transparent, 255 is opaque.
14  pub a: u8,
15}
16
17impl Color {
18  /// An opaque color.
19  pub const fn rgb(r: u8, g: u8, b: u8) -> Color {
20    Color { r, g, b, a: 255 }
21  }
22
23  /// A color with alpha: 0 is transparent, 255 is opaque.
24  pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
25    Color { r, g, b, a }
26  }
27
28  /// Parses `"#rrggbb"` or `"#rrggbbaa"`, in upper or lower case.
29  ///
30  /// # Panics
31  ///
32  /// If `hex` has any other form. In a `const`, that is a compile error:
33  ///
34  /// ```compile_fail
35  /// const BAD: crabshell_ui::Color = crabshell_ui::Color::hex("#12345");
36  /// ```
37  pub const fn hex(hex: &str) -> Color {
38    let bytes = hex.as_bytes();
39    if bytes.is_empty() || bytes[0] != b'#' || (bytes.len() != 7 && bytes.len() != 9) {
40      panic!("Color::hex takes \"#rrggbb\" or \"#rrggbbaa\"");
41    }
42    let a = if bytes.len() == 9 {
43      hex_byte(bytes[7], bytes[8])
44    } else {
45      255
46    };
47    Color {
48      r: hex_byte(bytes[1], bytes[2]),
49      g: hex_byte(bytes[3], bytes[4]),
50      b: hex_byte(bytes[5], bytes[6]),
51      a,
52    }
53  }
54}
55
56const fn hex_byte(high: u8, low: u8) -> u8 {
57  hex_digit(high) * 16 + hex_digit(low)
58}
59
60const fn hex_digit(digit: u8) -> u8 {
61  match digit {
62    b'0'..=b'9' => digit - b'0',
63    b'a'..=b'f' => digit - b'a' + 10,
64    b'A'..=b'F' => digit - b'A' + 10,
65    _ => panic!("Color::hex found a character that isn't a hex digit"),
66  }
67}
68
69/// A font: family, size and weight.
70///
71/// The family is resolved through fontconfig, so generic names such as
72/// `"sans-serif"` and `"monospace"` work too.
73#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct Font {
75  /// The family name, such as `"Inter"` or `"sans-serif"`.
76  pub family: &'static str,
77  /// The size in logical pixels.
78  pub size: f32,
79  /// The weight, from 100 (thin) through 400 (regular) and 700 (bold) to
80  /// 900 (black).
81  pub weight: u16,
82}
83
84impl Font {
85  /// The font text uses when no ancestor sets one.
86  pub const DEFAULT: Font = Font::new("sans-serif", 13.0);
87
88  /// A regular-weight font of `family` at `size` logical pixels.
89  pub const fn new(family: &'static str, size: f32) -> Font {
90    Font {
91      family,
92      size,
93      weight: 400,
94    }
95  }
96
97  /// The same font with another weight, such as 700 for bold.
98  pub const fn weight(self, weight: u16) -> Font {
99    Font { weight, ..self }
100  }
101}
102
103/// A border drawn inside a widget's edge. It takes no layout space.
104#[derive(Clone, Copy, Debug, PartialEq)]
105pub struct Border {
106  /// The width in logical pixels.
107  pub width: f32,
108  /// The color.
109  pub color: Color,
110}
111
112/// Space between a widget's edge and its content, in logical pixels.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114pub struct Padding {
115  /// Above and below the content.
116  pub vertical: u32,
117  /// Left and right of the content.
118  pub horizontal: u32,
119}
120
121/// How a widget looks. Every field is optional, and an unset field falls back
122/// to the widget's default: no background, black text in
123/// [`Font::DEFAULT`], no padding, square corners, no border, fully opaque.
124///
125/// [`Text`](crate::Text) takes `color` and `font` from the nearest widget
126/// that sets them, starting with itself and going up. Nothing else inherits.
127///
128/// `font` and `padding` change the layout; the other fields only change how
129/// the widget is painted.
130///
131/// ```
132/// use crabshell_ui::{Color, Font, Style};
133///
134/// const BAR: Style = Style::new()
135///     .background(Color::rgb(0x1e, 0x1e, 0x2e))
136///     .color(Color::hex("#cdd6f4"))
137///     .font(Font::new("Inter", 13.0));
138/// const WS: Style = Style::new().padding(2, 8).radius(4.0);
139/// const WS_FOCUSED: Style = WS.background(Color::rgb(0x45, 0x47, 0x5a));
140/// ```
141#[derive(Clone, Copy, Debug, Default, PartialEq)]
142pub struct Style {
143  /// Fills the widget's rectangle.
144  pub background: Option<Color>,
145  /// The text color, inherited by [`Text`](crate::Text).
146  pub color: Option<Color>,
147  /// The text font, inherited by [`Text`](crate::Text).
148  pub font: Option<Font>,
149  /// Space between the edge and the content.
150  pub padding: Option<Padding>,
151  /// The corner radius in logical pixels.
152  pub radius: Option<f32>,
153  /// A border inside the edge.
154  pub border: Option<Border>,
155  /// From 0.0 (invisible) to 1.0 (opaque), applied to the widget and
156  /// everything inside it.
157  pub opacity: Option<f32>,
158}
159
160impl Style {
161  /// A style that sets nothing.
162  pub const fn new() -> Style {
163    Style {
164      background: None,
165      color: None,
166      font: None,
167      padding: None,
168      radius: None,
169      border: None,
170      opacity: None,
171    }
172  }
173
174  /// Sets the background color.
175  pub const fn background(self, color: Color) -> Style {
176    Style {
177      background: Some(color),
178      ..self
179    }
180  }
181
182  /// Sets the text color.
183  pub const fn color(self, color: Color) -> Style {
184    Style {
185      color: Some(color),
186      ..self
187    }
188  }
189
190  /// Sets the text font.
191  pub const fn font(self, font: Font) -> Style {
192    Style {
193      font: Some(font),
194      ..self
195    }
196  }
197
198  /// Sets the padding: `vertical` above and below, `horizontal` left and
199  /// right, in logical pixels.
200  pub const fn padding(self, vertical: u32, horizontal: u32) -> Style {
201    Style {
202      padding: Some(Padding {
203        vertical,
204        horizontal,
205      }),
206      ..self
207    }
208  }
209
210  /// Sets the corner radius in logical pixels.
211  pub const fn radius(self, radius: f32) -> Style {
212    Style {
213      radius: Some(radius),
214      ..self
215    }
216  }
217
218  /// Sets a border of `width` logical pixels.
219  pub const fn border(self, width: f32, color: Color) -> Style {
220    Style {
221      border: Some(Border { width, color }),
222      ..self
223    }
224  }
225
226  /// Sets the opacity, from 0.0 to 1.0.
227  pub const fn opacity(self, opacity: f32) -> Style {
228    Style {
229      opacity: Some(opacity),
230      ..self
231    }
232  }
233
234  /// This style with every field that `over` sets replaced by its value.
235  /// Buttons use it to lay their hover and pressed styles over the normal
236  /// one.
237  pub const fn merge(self, over: Style) -> Style {
238    Style {
239      background: or(over.background, self.background),
240      color: or(over.color, self.color),
241      font: or(over.font, self.font),
242      padding: or(over.padding, self.padding),
243      radius: or(over.radius, self.radius),
244      border: or(over.border, self.border),
245      opacity: or(over.opacity, self.opacity),
246    }
247  }
248
249  /// Whether the two styles lay out the same: same font and padding.
250  pub(crate) fn same_layout(&self, other: &Style) -> bool {
251    self.font == other.font && self.padding == other.padding
252  }
253}
254
255/// `Option::or`, which isn't a `const fn` on stable Rust.
256const fn or<T: Copy>(first: Option<T>, second: Option<T>) -> Option<T> {
257  match first {
258    Some(value) => Some(value),
259    None => second,
260  }
261}