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