Skip to main content

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) -> bool {
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 using 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 colors(&self) -> &ColorsLinear {
268        self.cols
269    }
270
271    fn draw_rounded(&mut self) -> Option<&mut dyn DrawRounded> {
272        Some(&mut self.draw)
273    }
274
275    fn new_pass<'b>(
276        &mut self,
277        inner_rect: Rect,
278        offset: Offset,
279        class: PassType,
280        f: Box<dyn FnOnce(&mut dyn ThemeDraw) + 'b>,
281    ) {
282        let mut shadow = Default::default();
283        let mut outer_rect = inner_rect;
284        if class == PassType::Overlay {
285            shadow = Quad::conv(inner_rect);
286            shadow.a += self.w.dims.shadow_a * SHADOW_POPUP;
287            shadow.b += self.w.dims.shadow_b * SHADOW_POPUP;
288            let a = Coord::conv_floor(shadow.a);
289            let b = Coord::conv_ceil(shadow.b);
290            outer_rect = Rect::new(a, (b - a).cast());
291        }
292        let mut draw = self.draw.new_pass(outer_rect, offset, class);
293
294        if class == PassType::Overlay {
295            shadow += offset.cast();
296            let inner = Quad::conv(inner_rect + offset).shrink(self.w.dims.menu_frame as f32);
297            draw.rounded_frame_2col(shadow, inner, Rgba::BLACK, Rgba::TRANSPARENT);
298        }
299
300        let mut handle = DrawHandle {
301            draw,
302            ev: self.ev,
303            w: self.w,
304            cols: self.cols,
305        };
306        f(&mut handle);
307    }
308
309    fn event_state_overlay(&mut self) {
310        if let Some((coord, used)) = self.ev.mouse_pin() {
311            let center = coord.round().cast_approx();
312            let (r, inner) = match used {
313                false => (self.w.dims.scale * 3.6, 0.0),
314                true => (self.w.dims.scale * 6.0, 0.6),
315            };
316            let c = self.cols.accent;
317            self.draw.circle(Quad::from_center(center, r), inner, c);
318        }
319    }
320
321    fn frame(&mut self, id: &Id, rect: Rect, style: FrameStyle, bg: Background) {
322        let outer = Quad::conv(rect);
323        match style {
324            FrameStyle::None => {
325                let state = InputState::new_except_depress(self.ev, id);
326                let col = self.cols.from_bg(bg, state, false);
327                self.draw.rect(outer, col);
328            }
329            FrameStyle::Frame => {
330                let inner = outer.shrink(self.w.dims.frame as f32);
331                self.draw
332                    .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame);
333            }
334            FrameStyle::Window => {
335                let inner = outer.shrink(self.w.dims.frame_window as f32);
336                self.draw
337                    .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame);
338            }
339            FrameStyle::Popup => {
340                // We cheat here by using zero-sized popup-frame, but assuming that contents are
341                // all a MenuEntry, and drawing into this space. This might look wrong if other
342                // widgets are used in the popup.
343                let size = self.w.dims.menu_frame as f32;
344                let inner = outer.shrink(size);
345                self.draw
346                    .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame);
347                let inner = outer.shrink(size * BG_SHRINK_FACTOR);
348                self.draw.rect(inner, self.cols.background);
349            }
350            FrameStyle::MenuEntry => {
351                let state = InputState::new_all(self.ev, id);
352                if let Some(col) = self.cols.menu_entry(state) {
353                    let size = self.w.dims.menu_frame as f32;
354                    let inner = outer.shrink(size);
355                    self.draw.rounded_frame(outer, inner, BG_SHRINK_FACTOR, col);
356                    let inner = outer.shrink(size * BG_SHRINK_FACTOR);
357                    self.draw.rect(inner, col);
358                }
359            }
360            FrameStyle::NavFocus => {
361                let state = InputState::new_all(self.ev, id);
362                if let Some(col) = self.cols.nav_region(state) {
363                    let inner = outer.shrink(self.w.dims.m_inner as f32);
364                    self.draw.rounded_frame(outer, inner, 0.0, col);
365                }
366            }
367            FrameStyle::Button | FrameStyle::InvisibleButton => {
368                let state = InputState::new_all(self.ev, id);
369                if style == FrameStyle::InvisibleButton && !state.under_mouse() {
370                    return;
371                }
372
373                let outer = Quad::conv(rect);
374                let inner = outer.shrink(self.w.dims.button_frame as f32);
375
376                let col_bg = self.cols.from_bg(bg, state, false);
377                let col_frame = self.cols.nav_region(state).unwrap_or(self.cols.frame);
378
379                let shadow = match () {
380                    () if (self.cols.is_dark || state.disabled() || state.depress()) => {
381                        ShadowStyle::None
382                    }
383                    () if state.under_mouse() => ShadowStyle::MouseOver,
384                    _ => ShadowStyle::Normal,
385                };
386
387                self.button_frame(outer, inner, col_frame, col_bg, shadow);
388            }
389            FrameStyle::Tab => {
390                let state = InputState::new_all(self.ev, id);
391                let outer = Quad::conv(rect);
392                let w = self.w.dims.button_frame as f32;
393                let inner = Quad::from_coords(outer.a + w, outer.b - Vec2(w, 0.0));
394
395                let col_bg = self.cols.from_bg(bg, state, false);
396                let col_frame = self.cols.nav_region(state).unwrap_or(self.cols.frame);
397
398                self.button_frame(outer, inner, col_frame, col_bg, ShadowStyle::None);
399            }
400            FrameStyle::EditBox => self.edit_box(id, outer, bg),
401        }
402    }
403
404    fn check_box(&mut self, id: &Id, rect: Rect, checked: bool, last_change: Option<Instant>) {
405        let state = InputState::new_all(self.ev, id);
406        let outer = Quad::conv(rect);
407        let inner = outer.shrink(self.w.dims.button_frame as f32);
408
409        let col_frame = self.cols.nav_region(state).unwrap_or(self.cols.frame);
410        let col_bg = self.cols.from_edit_bg(Default::default(), state);
411
412        let shadow = match () {
413            () if (self.cols.is_dark || state.disabled() || state.depress()) => ShadowStyle::None,
414            () if state.under_mouse() => ShadowStyle::MouseOver,
415            _ => ShadowStyle::Normal,
416        };
417
418        let inner = self.button_frame(outer, inner, col_frame, col_bg, shadow);
419
420        self.check_mark(inner, state, checked, last_change);
421    }
422
423    fn radio_box(&mut self, id: &Id, rect: Rect, checked: bool, last_change: Option<Instant>) {
424        let anim_fade = 1.0 - self.w.anim.fade_bool(self.draw.draw, checked, last_change);
425
426        let state = InputState::new_all(self.ev, id);
427        let outer = Quad::conv(rect);
428        let col = self.cols.nav_region(state).unwrap_or(self.cols.frame);
429
430        if !(self.cols.is_dark || state.disabled() || state.depress()) {
431            let (mut a, mut b) = (self.w.dims.shadow_a, self.w.dims.shadow_b);
432            let mut mult = 0.65;
433            if state.under_mouse() {
434                mult *= SHADOW_MOUSE_OVER;
435            }
436            a *= mult;
437            b *= mult;
438            let shadow_outer = Quad::from_coords(a + outer.a, b + outer.b);
439            let col1 = if self.cols.is_dark { col } else { Rgba::BLACK };
440            let mut col2 = col1;
441            col2.a = 0.0;
442            self.draw.circle_2col(shadow_outer, col1, col2);
443        }
444
445        let col_bg = self.cols.from_edit_bg(Default::default(), state);
446        self.draw.circle(outer, 0.0, col_bg);
447
448        const F: f32 = 2.0 * (1.0 - BG_SHRINK_FACTOR); // match check box frame
449        let r = 1.0 - F * self.w.dims.button_frame as f32 / rect.size.0 as f32;
450        self.draw.circle(outer, r, col);
451
452        if anim_fade < 1.0 {
453            let r = self.w.dims.button_frame + self.w.dims.m_inner as i32;
454            let inner = outer.shrink(r as f32);
455            let v = inner.size() * (anim_fade / 2.0);
456            let inner = Quad::from_coords(inner.a + v, inner.b - v);
457            let col = self.cols.check_mark_state(state);
458            self.draw.circle(inner, 0.0, col);
459        }
460    }
461
462    fn scroll_bar(&mut self, id: &Id, id2: &Id, rect: Rect, h_rect: Rect, _: Direction) {
463        // track
464        let outer = Quad::conv(rect);
465        let inner = outer.shrink(outer.size().min_comp() / 2.0);
466        let mut col = self.cols.frame;
467        col.a = 0.5; // HACK
468        self.draw.rounded_frame(outer, inner, 0.0, col);
469
470        // grip
471        let outer = Quad::conv(h_rect);
472        let r = outer.size().min_comp() * 0.125;
473        let outer = outer.shrink(r);
474        let inner = outer.shrink(3.0 * r);
475        let state = InputState::new2(self.ev, id, id2);
476        let col = self.cols.accent_soft_state(state);
477        self.draw.rounded_frame(outer, inner, 0.0, col);
478    }
479
480    fn slider(&mut self, id: &Id, id2: &Id, rect: Rect, h_rect: Rect, dir: Direction) {
481        let state = InputState::new2(self.ev, id, id2);
482
483        // track
484        let mut outer = Quad::conv(rect);
485        let mid = Vec2::conv(h_rect.pos + h_rect.size / 2);
486        let (mut first, mut second);
487        if dir.is_horizontal() {
488            outer = outer.shrink_vec(Vec2(0.0, outer.size().1 * (1.0 / 3.0)));
489            first = Quad::from_coords(outer.a, Vec2(mid.0, outer.b.1));
490            second = Quad::from_coords(Vec2(mid.0, outer.a.1), outer.b);
491        } else {
492            outer = outer.shrink_vec(Vec2(outer.size().0 * (1.0 / 3.0), 0.0));
493            first = Quad::from_coords(outer.a, Vec2(outer.b.0, mid.1));
494            second = Quad::from_coords(Vec2(outer.a.0, mid.1), outer.b);
495        };
496        if dir.is_reversed() {
497            std::mem::swap(&mut first, &mut second);
498        }
499
500        let inner = first.shrink(first.size().min_comp() / 2.0);
501        self.draw.rounded_frame(first, inner, 0.0, self.cols.accent);
502        let inner = second.shrink(second.size().min_comp() / 2.0);
503        self.draw
504            .rounded_frame(second, inner, 1.0 / 3.0, self.cols.frame);
505
506        // grip; force it to be square
507        let size = Size::splat(h_rect.size.0.min(h_rect.size.1));
508        let offset = Offset::conv((h_rect.size - size) / 2);
509        let outer = Quad::conv(Rect::new(h_rect.pos + offset, size));
510
511        let col = if state.nav_focus() && !state.disabled() {
512            self.cols.accent_soft
513        } else {
514            self.cols.background
515        };
516        let col = ColorsLinear::adjust_for_state(col, state);
517
518        if !self.cols.is_dark && !state.contains(InputState::DISABLED | InputState::DEPRESS) {
519            let (mut a, mut b) = (self.w.dims.shadow_a, self.w.dims.shadow_b);
520            let mut mult = 0.6;
521            if state.under_mouse() {
522                mult *= SHADOW_MOUSE_OVER;
523            }
524            a *= mult;
525            b *= mult;
526            let shadow_outer = Quad::from_coords(a + outer.a, b + outer.b);
527            let col1 = if self.cols.is_dark { col } else { Rgba::BLACK };
528            let mut col2 = col1;
529            col2.a = 0.0;
530            self.draw.circle_2col(shadow_outer, col1, col2);
531        }
532
533        self.draw.circle(outer, 0.0, col);
534        let col = self.cols.nav_region(state).unwrap_or(self.cols.frame);
535        self.draw.circle(outer, 14.0 / 16.0, col);
536    }
537
538    fn progress_bar(&mut self, _: &Id, rect: Rect, dir: Direction, value: f32) {
539        let mut outer = Quad::conv(rect);
540        let inner = outer.shrink(outer.size().min_comp() / 2.0);
541        self.draw.rounded_frame(outer, inner, 0.75, self.cols.frame);
542
543        if dir.is_horizontal() {
544            outer.b.0 = outer.a.0 + value * (outer.b.0 - outer.a.0);
545        } else {
546            outer.b.1 = outer.a.1 + value * (outer.b.1 - outer.a.1);
547        }
548        let inner = outer.shrink(outer.size().min_comp() / 2.0);
549        self.draw.rounded_frame(outer, inner, 0.0, self.cols.accent);
550    }
551}