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}