macroquad/ui/
style.rs

1use crate::{
2    color::Color,
3    math::RectOffset,
4    text::{
5        atlas::{Atlas, SpriteKey},
6        Font,
7    },
8    texture::Image,
9    ui::ElementState,
10    Error,
11};
12
13use std::{
14    collections::HashMap,
15    sync::{Arc, Mutex},
16};
17
18pub struct StyleBuilder {
19    atlas: Arc<Mutex<Atlas>>,
20    font: Arc<Mutex<Font>>,
21    font_size: u16,
22    text_color: Color,
23    text_color_hovered: Color,
24    text_color_clicked: Color,
25    background_margin: Option<RectOffset>,
26    margin: Option<RectOffset>,
27    background: Option<Image>,
28    background_hovered: Option<Image>,
29    background_clicked: Option<Image>,
30    color: Color,
31    color_inactive: Option<Color>,
32    color_hovered: Color,
33    color_selected: Color,
34    color_selected_hovered: Color,
35    color_clicked: Color,
36    reverse_background_z: bool,
37}
38
39impl StyleBuilder {
40    pub(crate) fn new(default_font: Arc<Mutex<Font>>, atlas: Arc<Mutex<Atlas>>) -> StyleBuilder {
41        StyleBuilder {
42            atlas,
43            font: default_font,
44            font_size: 16,
45            text_color: Color::from_rgba(0, 0, 0, 255),
46            text_color_hovered: Color::from_rgba(0, 0, 0, 255),
47            text_color_clicked: Color::from_rgba(0, 0, 0, 255),
48            color: Color::from_rgba(255, 255, 255, 255),
49            color_hovered: Color::from_rgba(255, 255, 255, 255),
50            color_clicked: Color::from_rgba(255, 255, 255, 255),
51            color_selected: Color::from_rgba(255, 255, 255, 255),
52            color_selected_hovered: Color::from_rgba(255, 255, 255, 255),
53            color_inactive: None,
54            background: None,
55            background_margin: None,
56            margin: None,
57            background_hovered: None,
58            background_clicked: None,
59            reverse_background_z: false,
60        }
61    }
62
63    pub fn with_font(self, font: &Font) -> Result<StyleBuilder, Error> {
64        let mut font = font.clone();
65        font.set_atlas(self.atlas.clone());
66        font.set_characters(Arc::new(Mutex::new(HashMap::new())));
67        Ok(StyleBuilder {
68            font: Arc::new(Mutex::new(font)),
69            ..self
70        })
71    }
72
73    pub fn font(self, ttf_bytes: &[u8]) -> Result<StyleBuilder, Error> {
74        let font = Font::load_from_bytes(self.atlas.clone(), ttf_bytes)?;
75
76        Ok(StyleBuilder {
77            font: Arc::new(Mutex::new(font)),
78            ..self
79        })
80    }
81
82    pub fn background(self, background: Image) -> StyleBuilder {
83        StyleBuilder {
84            background: Some(background),
85            ..self
86        }
87    }
88
89    pub fn margin(self, margin: RectOffset) -> StyleBuilder {
90        StyleBuilder {
91            margin: Some(margin),
92            ..self
93        }
94    }
95
96    pub fn background_margin(self, margin: RectOffset) -> StyleBuilder {
97        StyleBuilder {
98            background_margin: Some(margin),
99            ..self
100        }
101    }
102
103    pub fn background_hovered(self, background_hovered: Image) -> StyleBuilder {
104        StyleBuilder {
105            background_hovered: Some(background_hovered),
106            ..self
107        }
108    }
109
110    pub fn background_clicked(self, background_clicked: Image) -> StyleBuilder {
111        StyleBuilder {
112            background_clicked: Some(background_clicked),
113            ..self
114        }
115    }
116
117    pub fn text_color(self, color: Color) -> StyleBuilder {
118        StyleBuilder {
119            text_color: color,
120            ..self
121        }
122    }
123
124    pub fn text_color_hovered(self, color_hovered: Color) -> StyleBuilder {
125        StyleBuilder {
126            text_color_hovered: color_hovered,
127            ..self
128        }
129    }
130
131    pub fn text_color_clicked(self, color_clicked: Color) -> StyleBuilder {
132        StyleBuilder {
133            text_color_clicked: color_clicked,
134            ..self
135        }
136    }
137
138    pub fn font_size(self, font_size: u16) -> StyleBuilder {
139        StyleBuilder { font_size, ..self }
140    }
141
142    pub fn color(self, color: Color) -> StyleBuilder {
143        StyleBuilder { color, ..self }
144    }
145
146    pub fn color_hovered(self, color_hovered: Color) -> StyleBuilder {
147        StyleBuilder {
148            color_hovered,
149            ..self
150        }
151    }
152
153    pub fn color_clicked(self, color_clicked: Color) -> StyleBuilder {
154        StyleBuilder {
155            color_clicked,
156            ..self
157        }
158    }
159
160    pub fn color_selected(self, color_selected: Color) -> StyleBuilder {
161        StyleBuilder {
162            color_selected,
163            ..self
164        }
165    }
166
167    pub fn color_selected_hovered(self, color_selected_hovered: Color) -> StyleBuilder {
168        StyleBuilder {
169            color_selected_hovered,
170            ..self
171        }
172    }
173
174    pub fn color_inactive(self, color_inactive: Color) -> StyleBuilder {
175        StyleBuilder {
176            color_inactive: Some(color_inactive),
177            ..self
178        }
179    }
180
181    pub fn reverse_background_z(self, reverse_background_z: bool) -> StyleBuilder {
182        StyleBuilder {
183            reverse_background_z,
184            ..self
185        }
186    }
187
188    pub fn build(self) -> Style {
189        let mut atlas = self.atlas.lock().unwrap();
190
191        let background = self.background.map(|image| {
192            let id = atlas.new_unique_id();
193            atlas.cache_sprite(id, image);
194            id
195        });
196
197        let background_hovered = self.background_hovered.map(|image| {
198            let id = atlas.new_unique_id();
199            atlas.cache_sprite(id, image);
200            id
201        });
202
203        let background_clicked = self.background_clicked.map(|image| {
204            let id = atlas.new_unique_id();
205            atlas.cache_sprite(id, image);
206            id
207        });
208
209        Style {
210            background_margin: self.background_margin,
211            margin: self.margin,
212            background,
213            background_hovered,
214            background_clicked,
215            color: self.color,
216            color_hovered: self.color_hovered,
217            color_clicked: self.color_clicked,
218            color_inactive: self.color_inactive,
219            color_selected: self.color_selected,
220            color_selected_hovered: self.color_selected_hovered,
221            font: self.font,
222            text_color: self.text_color,
223            text_color_hovered: self.text_color_hovered,
224            text_color_clicked: self.text_color_clicked,
225            font_size: self.font_size,
226            reverse_background_z: self.reverse_background_z,
227        }
228    }
229}
230
231#[derive(Debug, Clone)]
232pub struct Style {
233    pub(crate) background: Option<SpriteKey>,
234    pub(crate) background_hovered: Option<SpriteKey>,
235    pub(crate) background_clicked: Option<SpriteKey>,
236    pub(crate) color: Color,
237    pub(crate) color_inactive: Option<Color>,
238    pub(crate) color_hovered: Color,
239    pub(crate) color_clicked: Color,
240    pub(crate) color_selected: Color,
241    pub(crate) color_selected_hovered: Color,
242    /// Margins of background image
243    /// Applies to background/background_hovered/background_clicked etc
244    /// Part of the texture within the margin would not be scaled, which is useful
245    /// for things like element borders
246    pub(crate) background_margin: Option<RectOffset>,
247    /// Margin that do not affect textures
248    /// Useful to leave some empty space between element border and element content
249    /// Maybe be negative to compensate background_margin when content should overlap the
250    /// borders
251    pub(crate) margin: Option<RectOffset>,
252    pub(crate) font: Arc<Mutex<Font>>,
253    pub(crate) text_color: Color,
254    pub(crate) text_color_hovered: Color,
255    pub(crate) text_color_clicked: Color,
256    pub(crate) font_size: u16,
257    pub(crate) reverse_background_z: bool,
258}
259
260impl Style {
261    fn default(font: Arc<Mutex<Font>>) -> Style {
262        Style {
263            background: None,
264            background_margin: None,
265            margin: None,
266            background_hovered: None,
267            background_clicked: None,
268            font,
269            text_color: Color::from_rgba(0, 0, 0, 255),
270            text_color_hovered: Color::from_rgba(0, 0, 0, 255),
271            text_color_clicked: Color::from_rgba(0, 0, 0, 255),
272            font_size: 16,
273            color: Color::from_rgba(255, 255, 255, 255),
274            color_hovered: Color::from_rgba(255, 255, 255, 255),
275            color_clicked: Color::from_rgba(255, 255, 255, 255),
276            color_selected: Color::from_rgba(255, 255, 255, 255),
277            color_selected_hovered: Color::from_rgba(255, 255, 255, 255),
278            color_inactive: None,
279            reverse_background_z: false,
280        }
281    }
282
283    pub(crate) fn border_margin(&self) -> RectOffset {
284        let background_offset = self.background_margin.unwrap_or_default();
285        let background = self.margin.unwrap_or_default();
286
287        RectOffset {
288            left: background_offset.left + background.left,
289            right: background_offset.right + background.right,
290            top: background_offset.top + background.top,
291            bottom: background_offset.bottom + background.bottom,
292        }
293    }
294
295    pub(crate) fn text_color(&self, element_state: ElementState) -> Color {
296        let ElementState {
297            focused,
298            hovered,
299            clicked,
300            ..
301        } = element_state;
302
303        if clicked {
304            self.text_color_clicked
305        } else if hovered {
306            self.text_color_hovered
307        } else if focused {
308            self.text_color
309        } else {
310            Color::new(
311                self.text_color.r * 0.6,
312                self.text_color.g * 0.6,
313                self.text_color.b * 0.6,
314                self.text_color.a * 0.6,
315            )
316        }
317    }
318
319    pub(crate) fn color(&self, element_state: ElementState) -> Color {
320        let ElementState {
321            clicked,
322            hovered,
323            focused,
324            selected,
325        } = element_state;
326
327        if focused == false {
328            return self.color_inactive.unwrap_or(Color::from_rgba(
329                (self.color.r * 255.) as u8,
330                (self.color.g * 255.) as u8,
331                (self.color.b * 255.) as u8,
332                (self.color.a * 255. * 0.8) as u8,
333            ));
334        }
335        if clicked {
336            return self.color_clicked;
337        }
338        if selected && hovered {
339            return self.color_selected_hovered;
340        }
341
342        if selected {
343            return self.color_selected;
344        }
345        if hovered {
346            return self.color_hovered;
347        }
348
349        self.color
350    }
351
352    pub(crate) const fn background_sprite(&self, element_state: ElementState) -> Option<SpriteKey> {
353        let ElementState {
354            clicked, hovered, ..
355        } = element_state;
356
357        if clicked && self.background_clicked.is_some() {
358            return self.background_clicked;
359        }
360
361        if hovered && self.background_hovered.is_some() {
362            return self.background_hovered;
363        }
364
365        self.background
366    }
367}
368
369#[derive(Debug, Clone)]
370pub struct Skin {
371    pub label_style: Style,
372    pub button_style: Style,
373    pub tabbar_style: Style,
374    pub combobox_style: Style,
375    pub window_style: Style,
376    pub editbox_style: Style,
377    pub window_titlebar_style: Style,
378    pub scrollbar_style: Style,
379    pub scrollbar_handle_style: Style,
380    pub checkbox_style: Style,
381    pub group_style: Style,
382
383    pub margin: f32,
384    pub title_height: f32,
385
386    pub scroll_width: f32,
387    pub scroll_multiplier: f32,
388}
389
390impl Skin {
391    pub(crate) fn new(atlas: Arc<Mutex<Atlas>>, default_font: Arc<Mutex<Font>>) -> Self {
392        Skin {
393            label_style: Style {
394                margin: Some(RectOffset::new(2., 2., 2., 2.)),
395                text_color: Color::from_rgba(0, 0, 0, 255),
396                color_inactive: Some(Color::from_rgba(0, 0, 0, 128)),
397                ..Style::default(default_font.clone())
398            },
399            button_style: Style {
400                margin: Some(RectOffset::new(2., 2., 2., 2.)),
401                color: Color::from_rgba(204, 204, 204, 235),
402                color_clicked: Color::from_rgba(187, 187, 187, 255),
403                color_hovered: Color::from_rgba(170, 170, 170, 235),
404                text_color: Color::from_rgba(0, 0, 0, 255),
405                ..Style::default(default_font.clone())
406            },
407            combobox_style: StyleBuilder::new(default_font.clone(), atlas.clone())
408                .background_margin(RectOffset::new(1., 14., 1., 1.))
409                .color_inactive(Color::from_rgba(238, 238, 238, 128))
410                .text_color(Color::from_rgba(0, 0, 0, 255))
411                .color(Color::from_rgba(220, 220, 220, 255))
412                .background(Image {
413                    width: 16,
414                    height: 30,
415                    bytes: include_bytes!("combobox.img").to_vec(),
416                })
417                .build(),
418            tabbar_style: Style {
419                margin: Some(RectOffset::new(2., 2., 2., 2.)),
420                color: Color::from_rgba(220, 220, 220, 235),
421                color_clicked: Color::from_rgba(187, 187, 187, 235),
422                color_hovered: Color::from_rgba(170, 170, 170, 235),
423                color_selected_hovered: Color::from_rgba(180, 180, 180, 235),
424                color_selected: Color::from_rgba(204, 204, 204, 235),
425                text_color: Color::from_rgba(0, 0, 0, 255),
426                ..Style::default(default_font.clone())
427            },
428            window_style: StyleBuilder::new(default_font.clone(), atlas.clone())
429                .background_margin(RectOffset::new(1., 1., 1., 1.))
430                .color_inactive(Color::from_rgba(238, 238, 238, 128))
431                .text_color(Color::from_rgba(0, 0, 0, 255))
432                .background(Image {
433                    width: 3,
434                    height: 3,
435                    bytes: vec![
436                        68, 68, 68, 255, 68, 68, 68, 255, 68, 68, 68, 255, 68, 68, 68, 255, 238,
437                        238, 238, 255, 68, 68, 68, 255, 68, 68, 68, 255, 68, 68, 68, 255, 68, 68,
438                        68, 255,
439                    ],
440                })
441                .build(),
442            window_titlebar_style: Style {
443                color: Color::from_rgba(68, 68, 68, 255),
444                color_inactive: Some(Color::from_rgba(102, 102, 102, 127)),
445                text_color: Color::from_rgba(0, 0, 0, 255),
446                ..Style::default(default_font.clone())
447            },
448            scrollbar_style: Style {
449                color: Color::from_rgba(68, 68, 68, 255),
450                ..Style::default(default_font.clone())
451            },
452            editbox_style: Style {
453                text_color: Color::from_rgba(0, 0, 0, 255),
454                color_selected: Color::from_rgba(200, 200, 200, 255),
455                ..Style::default(default_font.clone())
456            },
457
458            scrollbar_handle_style: Style {
459                color: Color::from_rgba(204, 204, 204, 235),
460                color_inactive: Some(Color::from_rgba(204, 204, 204, 128)),
461                color_hovered: Color::from_rgba(180, 180, 180, 235),
462                color_clicked: Color::from_rgba(170, 170, 170, 235),
463                ..Style::default(default_font.clone())
464            },
465            checkbox_style: Style {
466                text_color: Color::from_rgba(0, 0, 0, 255),
467                font_size: 16,
468                color: Color::from_rgba(200, 200, 200, 255),
469                color_hovered: Color::from_rgba(210, 210, 210, 255),
470                color_clicked: Color::from_rgba(150, 150, 150, 255),
471                color_selected: Color::from_rgba(128, 128, 128, 255),
472                color_selected_hovered: Color::from_rgba(140, 140, 140, 255),
473                ..Style::default(default_font.clone())
474            },
475            group_style: Style {
476                color: Color::from_rgba(34, 34, 34, 68),
477                color_hovered: Color::from_rgba(34, 153, 34, 68),
478                color_selected: Color::from_rgba(34, 34, 255, 255),
479                color_selected_hovered: Color::from_rgba(55, 55, 55, 68),
480                ..Style::default(default_font.clone())
481            },
482
483            margin: 2.0,
484            title_height: 14.0,
485            scroll_width: 10.0,
486            scroll_multiplier: 3.,
487        }
488    }
489}