kas_core/theme/
flat_theme.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Flat theme
7
8use std::cell::RefCell;
9use std::f32;
10use std::ops::Range;
11use std::time::Instant;
12
13use super::SimpleTheme;
14use crate::Id;
15use crate::cast::traits::*;
16use crate::config::{Config, WindowConfig};
17use crate::dir::{Direction, Directional};
18use crate::draw::{color::Rgba, *};
19use crate::event::EventState;
20use crate::geom::*;
21use crate::text::TextDisplay;
22use crate::theme::dimensions as dim;
23use crate::theme::{Background, FrameStyle, MarkStyle};
24use crate::theme::{ColorsLinear, InputState, Theme};
25use crate::theme::{ThemeDraw, ThemeSize};
26
27// Used to ensure a rectangular background is inside a circular corner.
28// Also the maximum inner radius of circular borders to overlap with this rect.
29const BG_SHRINK_FACTOR: f32 = 1.0 - std::f32::consts::FRAC_1_SQRT_2;
30
31// Shadow enlargement on mouse over
32const SHADOW_MOUSE_OVER: f32 = 1.1;
33// Shadow enlargement for pop-ups
34const SHADOW_POPUP: f32 = 1.2;
35
36#[derive(PartialEq, Eq)]
37enum ShadowStyle {
38    None,
39    Normal,
40    MouseOver,
41}
42
43/// A theme with flat (unshaded) rendering
44///
45/// This is a fully functional theme using only the basic drawing primitives
46/// available by default.
47#[derive(Clone, Debug)]
48pub struct FlatTheme {
49    base: SimpleTheme,
50}
51
52impl Default for FlatTheme {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl FlatTheme {
59    /// Construct
60    #[inline]
61    pub fn new() -> Self {
62        let base = SimpleTheme::new();
63        FlatTheme { base }
64    }
65}
66
67fn dimensions() -> dim::Parameters {
68    dim::Parameters {
69        // NOTE: visual thickness is (button_frame * scale_factor).round() * (1 - BG_SHRINK_FACTOR)
70        button_frame: 2.4,
71        button_inner: 0.0,
72        slider_size: Vec2(24.0, 18.0),
73        shadow_size: Vec2(4.0, 4.0),
74        shadow_rel_offset: Vec2(0.2, 0.3),
75        ..Default::default()
76    }
77}
78
79pub struct DrawHandle<'a, DS: DrawSharedImpl> {
80    pub(crate) draw: DrawIface<'a, DS>,
81    pub(crate) ev: &'a mut EventState,
82    pub(crate) w: &'a mut dim::Window<DS::Draw>,
83    pub(crate) cols: &'a ColorsLinear,
84}
85
86impl<DS: DrawSharedImpl> Theme<DS> for FlatTheme
87where
88    DS::Draw: DrawRoundedImpl,
89{
90    type Window = dim::Window<DS::Draw>;
91    type Draw<'a> = DrawHandle<'a, DS>;
92
93    fn init(&mut self, config: &RefCell<Config>) {
94        <SimpleTheme as Theme<DS>>::init(&mut self.base, config)
95    }
96
97    fn new_window(&mut self, config: &WindowConfig) -> Self::Window {
98        self.base.cols = config.theme().get_active_scheme().into();
99        dim::Window::new(&dimensions(), config)
100    }
101
102    fn update_window(&mut self, w: &mut Self::Window, config: &WindowConfig) {
103        self.base.cols = config.theme().get_active_scheme().into();
104        w.update(&dimensions(), config);
105    }
106
107    fn draw<'a>(
108        &'a self,
109        draw: DrawIface<'a, DS>,
110        ev: &'a mut EventState,
111        w: &'a mut Self::Window,
112    ) -> Self::Draw<'a> {
113        w.anim.update();
114
115        DrawHandle {
116            draw,
117            ev,
118            w,
119            cols: &self.base.cols,
120        }
121    }
122
123    fn draw_upcast<'a>(
124        draw: DrawIface<'a, DS>,
125        ev: &'a mut EventState,
126        w: &'a mut Self::Window,
127        cols: &'a ColorsLinear,
128    ) -> Self::Draw<'a> {
129        DrawHandle { draw, ev, w, cols }
130    }
131
132    fn clear_color(&self) -> Rgba {
133        self.base.cols.background
134    }
135}
136
137impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS>
138where
139    DS::Draw: DrawRoundedImpl,
140{
141    // Type-cast to simple_theme's DrawHandle. Should be equivalent to transmute.
142    fn as_simple<'b, 'c>(&'b mut self) -> super::simple_theme::DrawHandle<'c, DS>
143    where
144        'a: 'c,
145        'b: 'c,
146    {
147        super::simple_theme::DrawHandle {
148            draw: self.draw.re(),
149            ev: self.ev,
150            w: self.w,
151            cols: self.cols,
152        }
153    }
154
155    fn button_frame(
156        &mut self,
157        outer: Quad,
158        inner: Quad,
159        col_frame: Rgba,
160        col_bg: Rgba,
161        shadow: ShadowStyle,
162    ) -> Quad {
163        #[cfg(debug_assertions)]
164        {
165            if !(inner.a < inner.b) {
166                log::warn!("button_frame: frame too small: {outer:?}");
167            }
168        }
169
170        if shadow != ShadowStyle::None {
171            let (mut a, mut b) = (self.w.dims.shadow_a, self.w.dims.shadow_b);
172            if shadow == ShadowStyle::MouseOver {
173                a *= SHADOW_MOUSE_OVER;
174                b *= SHADOW_MOUSE_OVER;
175            }
176            let shadow_outer = Quad::from_coords(a + inner.a, b + inner.b);
177            let col1 = if self.cols.is_dark { col_frame } else { Rgba::BLACK };
178            let mut col2 = col1;
179            col2.a = 0.0;
180            self.draw
181                .rounded_frame_2col(shadow_outer, inner, col1, col2);
182        }
183
184        let bgr = outer.shrink(self.w.dims.button_frame as f32 * BG_SHRINK_FACTOR);
185        self.draw.rect(bgr, col_bg);
186
187        self.draw
188            .rounded_frame(outer, inner, BG_SHRINK_FACTOR, col_frame);
189        inner
190    }
191
192    pub fn edit_box(&mut self, id: &Id, outer: Quad, bg: Background) {
193        let state = InputState::new_except_depress(self.ev, id);
194        let col_bg = self.cols.from_edit_bg(bg, state);
195        if col_bg != self.cols.background {
196            let inner = outer.shrink(self.w.dims.button_frame as f32 * BG_SHRINK_FACTOR);
197            self.draw.rect(inner, col_bg);
198        }
199
200        let inner = outer.shrink(self.w.dims.button_frame as f32);
201        self.draw
202            .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame);
203
204        if !state.disabled() && !self.cols.is_dark && (state.nav_focus() || state.under_mouse()) {
205            let r = 0.5 * self.w.dims.button_frame as f32;
206            let y = outer.b.1 - r;
207            let a = Vec2(outer.a.0 + r, y);
208            let b = Vec2(outer.b.0 - r, y);
209            let col = if state.nav_focus() {
210                self.cols.nav_focus
211            } else {
212                self.cols.text
213            };
214
215            const F: f32 = 0.6;
216            let (sa, sb) = (self.w.dims.shadow_a * F, self.w.dims.shadow_b * F);
217            let outer = Quad::from_coords(a + sa, b + sb);
218            let inner = Quad::from_coords(a, b);
219            let col1 = if self.cols.is_dark { col } else { Rgba::BLACK };
220            let mut col2 = col1;
221            col2.a = 0.0;
222            self.draw.rounded_frame_2col(outer, inner, col1, col2);
223
224            self.draw.rounded_line(a, b, r, col);
225        }
226    }
227
228    pub fn check_mark(
229        &mut self,
230        inner: Quad,
231        state: InputState,
232        checked: bool,
233        last_change: Option<Instant>,
234    ) {
235        let anim_fade = 1.0 - self.w.anim.fade_bool(self.draw.draw, checked, last_change);
236        if anim_fade < 1.0 {
237            let inner = inner.shrink(self.w.dims.m_inner as f32);
238            let v = inner.size() * (anim_fade / 2.0);
239            let inner = Quad::from_coords(inner.a + v, inner.b - v);
240            let col = self.cols.check_mark_state(state);
241            let f = self.w.dims.mark_line;
242            if inner.size().min_comp() >= 2.0 * f {
243                let inner = inner.shrink(f);
244                let size = inner.size();
245                let vstep = size.1 * 0.125;
246                let a = Vec2(inner.a.0, inner.b.1 - 3.0 * vstep);
247                let b = Vec2(inner.a.0 + size.0 * 0.25, inner.b.1 - vstep);
248                let c = Vec2(inner.b.0, inner.a.1 + vstep);
249                self.draw.rounded_line(a, b, f, col);
250                self.draw.rounded_line(b, c, f, col);
251            } else {
252                self.draw.rect(inner, col);
253            }
254        }
255    }
256}
257
258#[kas::extends(ThemeDraw, base=self.as_simple())]
259impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS>
260where
261    DS::Draw: DrawRoundedImpl,
262{
263    fn components(&mut self) -> (&dyn ThemeSize, &mut dyn Draw, &mut EventState) {
264        (self.w, &mut self.draw, self.ev)
265    }
266
267    fn new_pass<'b>(
268        &mut self,
269        inner_rect: Rect,
270        offset: Offset,
271        class: PassType,
272        f: Box<dyn FnOnce(&mut dyn ThemeDraw) + 'b>,
273    ) {
274        let mut shadow = Default::default();
275        let mut outer_rect = inner_rect;
276        if class == PassType::Overlay {
277            shadow = Quad::conv(inner_rect);
278            shadow.a += self.w.dims.shadow_a * SHADOW_POPUP;
279            shadow.b += self.w.dims.shadow_b * SHADOW_POPUP;
280            let a = Coord::conv_floor(shadow.a);
281            let b = Coord::conv_ceil(shadow.b);
282            outer_rect = Rect::new(a, (b - a).cast());
283        }
284        let mut draw = self.draw.new_pass(outer_rect, offset, class);
285
286        if class == PassType::Overlay {
287            shadow += offset.cast();
288            let inner = Quad::conv(inner_rect + offset).shrink(self.w.dims.menu_frame as f32);
289            draw.rounded_frame_2col(shadow, inner, Rgba::BLACK, Rgba::TRANSPARENT);
290        }
291
292        let mut handle = DrawHandle {
293            draw,
294            ev: self.ev,
295            w: self.w,
296            cols: self.cols,
297        };
298        f(&mut handle);
299    }
300
301    fn frame(&mut self, id: &Id, rect: Rect, style: FrameStyle, bg: Background) {
302        let outer = Quad::conv(rect);
303        match style {
304            FrameStyle::None => {
305                let state = InputState::new_except_depress(self.ev, id);
306                let col = self.cols.from_bg(bg, state, false);
307                self.draw.rect(outer, col);
308            }
309            FrameStyle::Frame => {
310                let inner = outer.shrink(self.w.dims.frame as f32);
311                self.draw
312                    .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame);
313            }
314            FrameStyle::Window => {
315                let inner = outer.shrink(self.w.dims.frame_window as f32);
316                self.draw
317                    .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame);
318            }
319            FrameStyle::Popup => {
320                // We cheat here by using zero-sized popup-frame, but assuming that contents are
321                // all a MenuEntry, and drawing into this space. This might look wrong if other
322                // widgets are used in the popup.
323                let size = self.w.dims.menu_frame as f32;
324                let inner = outer.shrink(size);
325                self.draw
326                    .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame);
327                let inner = outer.shrink(size * BG_SHRINK_FACTOR);
328                self.draw.rect(inner, self.cols.background);
329            }
330            FrameStyle::MenuEntry => {
331                let state = InputState::new_all(self.ev, id);
332                if let Some(col) = self.cols.menu_entry(state) {
333                    let size = self.w.dims.menu_frame as f32;
334                    let inner = outer.shrink(size);
335                    self.draw.rounded_frame(outer, inner, BG_SHRINK_FACTOR, col);
336                    let inner = outer.shrink(size * BG_SHRINK_FACTOR);
337                    self.draw.rect(inner, col);
338                }
339            }
340            FrameStyle::NavFocus => {
341                let state = InputState::new_all(self.ev, id);
342                if let Some(col) = self.cols.nav_region(state) {
343                    let inner = outer.shrink(self.w.dims.m_inner as f32);
344                    self.draw.rounded_frame(outer, inner, 0.0, col);
345                }
346            }
347            FrameStyle::Button => {
348                let state = InputState::new_all(self.ev, id);
349                let outer = Quad::conv(rect);
350                let inner = outer.shrink(self.w.dims.button_frame as f32);
351
352                let col_bg = self.cols.from_bg(bg, state, false);
353                let col_frame = self.cols.nav_region(state).unwrap_or(self.cols.frame);
354
355                let shadow = match () {
356                    () if (self.cols.is_dark || state.disabled() || state.depress()) => {
357                        ShadowStyle::None
358                    }
359                    () if state.under_mouse() => ShadowStyle::MouseOver,
360                    _ => ShadowStyle::Normal,
361                };
362
363                self.button_frame(outer, inner, col_frame, col_bg, shadow);
364            }
365            FrameStyle::Tab => {
366                let state = InputState::new_all(self.ev, id);
367                let outer = Quad::conv(rect);
368                let w = self.w.dims.button_frame as f32;
369                let inner = Quad::from_coords(outer.a + w, outer.b - Vec2(w, 0.0));
370
371                let col_bg = self.cols.from_bg(bg, state, false);
372                let col_frame = self.cols.nav_region(state).unwrap_or(self.cols.frame);
373
374                self.button_frame(outer, inner, col_frame, col_bg, ShadowStyle::None);
375            }
376            FrameStyle::EditBox => self.edit_box(id, outer, bg),
377        }
378    }
379
380    fn check_box(&mut self, id: &Id, rect: Rect, checked: bool, last_change: Option<Instant>) {
381        let state = InputState::new_all(self.ev, id);
382        let outer = Quad::conv(rect);
383        let inner = outer.shrink(self.w.dims.button_frame as f32);
384
385        let col_frame = self.cols.nav_region(state).unwrap_or(self.cols.frame);
386        let col_bg = self.cols.from_edit_bg(Default::default(), state);
387
388        let shadow = match () {
389            () if (self.cols.is_dark || state.disabled() || state.depress()) => ShadowStyle::None,
390            () if state.under_mouse() => ShadowStyle::MouseOver,
391            _ => ShadowStyle::Normal,
392        };
393
394        let inner = self.button_frame(outer, inner, col_frame, col_bg, shadow);
395
396        self.check_mark(inner, state, checked, last_change);
397    }
398
399    fn radio_box(&mut self, id: &Id, rect: Rect, checked: bool, last_change: Option<Instant>) {
400        let anim_fade = 1.0 - self.w.anim.fade_bool(self.draw.draw, checked, last_change);
401
402        let state = InputState::new_all(self.ev, id);
403        let outer = Quad::conv(rect);
404        let col = self.cols.nav_region(state).unwrap_or(self.cols.frame);
405
406        if !(self.cols.is_dark || state.disabled() || state.depress()) {
407            let (mut a, mut b) = (self.w.dims.shadow_a, self.w.dims.shadow_b);
408            let mut mult = 0.65;
409            if state.under_mouse() {
410                mult *= SHADOW_MOUSE_OVER;
411            }
412            a *= mult;
413            b *= mult;
414            let shadow_outer = Quad::from_coords(a + outer.a, b + outer.b);
415            let col1 = if self.cols.is_dark { col } else { Rgba::BLACK };
416            let mut col2 = col1;
417            col2.a = 0.0;
418            self.draw.circle_2col(shadow_outer, col1, col2);
419        }
420
421        let col_bg = self.cols.from_edit_bg(Default::default(), state);
422        self.draw.circle(outer, 0.0, col_bg);
423
424        const F: f32 = 2.0 * (1.0 - BG_SHRINK_FACTOR); // match check box frame
425        let r = 1.0 - F * self.w.dims.button_frame as f32 / rect.size.0 as f32;
426        self.draw.circle(outer, r, col);
427
428        if anim_fade < 1.0 {
429            let r = self.w.dims.button_frame + self.w.dims.m_inner as i32;
430            let inner = outer.shrink(r as f32);
431            let v = inner.size() * (anim_fade / 2.0);
432            let inner = Quad::from_coords(inner.a + v, inner.b - v);
433            let col = self.cols.check_mark_state(state);
434            self.draw.circle(inner, 0.0, col);
435        }
436    }
437
438    fn scroll_bar(&mut self, id: &Id, id2: &Id, rect: Rect, h_rect: Rect, _: Direction) {
439        // track
440        let outer = Quad::conv(rect);
441        let inner = outer.shrink(outer.size().min_comp() / 2.0);
442        let mut col = self.cols.frame;
443        col.a = 0.5; // HACK
444        self.draw.rounded_frame(outer, inner, 0.0, col);
445
446        // grip
447        let outer = Quad::conv(h_rect);
448        let r = outer.size().min_comp() * 0.125;
449        let outer = outer.shrink(r);
450        let inner = outer.shrink(3.0 * r);
451        let state = InputState::new2(self.ev, id, id2);
452        let col = self.cols.accent_soft_state(state);
453        self.draw.rounded_frame(outer, inner, 0.0, col);
454    }
455
456    fn slider(&mut self, id: &Id, id2: &Id, rect: Rect, h_rect: Rect, dir: Direction) {
457        let state = InputState::new2(self.ev, id, id2);
458
459        // track
460        let mut outer = Quad::conv(rect);
461        let mid = Vec2::conv(h_rect.pos + h_rect.size / 2);
462        let (mut first, mut second);
463        if dir.is_horizontal() {
464            outer = outer.shrink_vec(Vec2(0.0, outer.size().1 * (1.0 / 3.0)));
465            first = Quad::from_coords(outer.a, Vec2(mid.0, outer.b.1));
466            second = Quad::from_coords(Vec2(mid.0, outer.a.1), outer.b);
467        } else {
468            outer = outer.shrink_vec(Vec2(outer.size().0 * (1.0 / 3.0), 0.0));
469            first = Quad::from_coords(outer.a, Vec2(outer.b.0, mid.1));
470            second = Quad::from_coords(Vec2(outer.a.0, mid.1), outer.b);
471        };
472        if dir.is_reversed() {
473            std::mem::swap(&mut first, &mut second);
474        }
475
476        let inner = first.shrink(first.size().min_comp() / 2.0);
477        self.draw.rounded_frame(first, inner, 0.0, self.cols.accent);
478        let inner = second.shrink(second.size().min_comp() / 2.0);
479        self.draw
480            .rounded_frame(second, inner, 1.0 / 3.0, self.cols.frame);
481
482        // grip; force it to be square
483        let size = Size::splat(h_rect.size.0.min(h_rect.size.1));
484        let offset = Offset::conv((h_rect.size - size) / 2);
485        let outer = Quad::conv(Rect::new(h_rect.pos + offset, size));
486
487        let col = if state.nav_focus() && !state.disabled() {
488            self.cols.accent_soft
489        } else {
490            self.cols.background
491        };
492        let col = ColorsLinear::adjust_for_state(col, state);
493
494        if !self.cols.is_dark && !state.contains(InputState::DISABLED | InputState::DEPRESS) {
495            let (mut a, mut b) = (self.w.dims.shadow_a, self.w.dims.shadow_b);
496            let mut mult = 0.6;
497            if state.under_mouse() {
498                mult *= SHADOW_MOUSE_OVER;
499            }
500            a *= mult;
501            b *= mult;
502            let shadow_outer = Quad::from_coords(a + outer.a, b + outer.b);
503            let col1 = if self.cols.is_dark { col } else { Rgba::BLACK };
504            let mut col2 = col1;
505            col2.a = 0.0;
506            self.draw.circle_2col(shadow_outer, col1, col2);
507        }
508
509        self.draw.circle(outer, 0.0, col);
510        let col = self.cols.nav_region(state).unwrap_or(self.cols.frame);
511        self.draw.circle(outer, 14.0 / 16.0, col);
512    }
513
514    fn progress_bar(&mut self, _: &Id, rect: Rect, dir: Direction, value: f32) {
515        let mut outer = Quad::conv(rect);
516        let inner = outer.shrink(outer.size().min_comp() / 2.0);
517        self.draw.rounded_frame(outer, inner, 0.75, self.cols.frame);
518
519        if dir.is_horizontal() {
520            outer.b.0 = outer.a.0 + value * (outer.b.0 - outer.a.0);
521        } else {
522            outer.b.1 = outer.a.1 + value * (outer.b.1 - outer.a.1);
523        }
524        let inner = outer.shrink(outer.size().min_comp() / 2.0);
525        self.draw.rounded_frame(outer, inner, 0.0, self.cols.accent);
526    }
527}