kas_theme/
shaded_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//! Shaded theme
7
8use std::f32;
9use std::ops::Range;
10use std::time::Instant;
11
12use crate::{dim, ColorsLinear, Config, InputState, SimpleTheme, Theme};
13use crate::{DrawShaded, DrawShadedImpl};
14use kas::cast::traits::*;
15use kas::dir::{Direction, Directional};
16use kas::draw::{color::Rgba, *};
17use kas::event::EventState;
18use kas::geom::*;
19use kas::text::{TextApi, TextDisplay};
20use kas::theme::{Background, ThemeControl, ThemeDraw, ThemeSize};
21use kas::theme::{FrameStyle, MarkStyle, TextClass};
22use kas::{TkAction, WidgetId};
23
24/// A theme using simple shading to give apparent depth to elements
25#[derive(Clone, Debug)]
26pub struct ShadedTheme {
27    base: SimpleTheme,
28}
29
30impl Default for ShadedTheme {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl ShadedTheme {
37    /// Construct
38    pub fn new() -> Self {
39        let base = SimpleTheme::new();
40        ShadedTheme { base }
41    }
42
43    /// Set font size
44    ///
45    /// Units: Points per Em (standard unit of font size)
46    #[must_use]
47    pub fn with_font_size(mut self, pt_size: f32) -> Self {
48        self.base.config.set_font_size(pt_size);
49        self
50    }
51
52    /// Set the colour scheme
53    ///
54    /// If no scheme by this name is found the scheme is left unchanged.
55    #[must_use]
56    pub fn with_colours(mut self, scheme: &str) -> Self {
57        self.base.config.set_active_scheme(scheme);
58        if let Some(scheme) = self.base.config.get_color_scheme(scheme) {
59            self.base.cols = scheme.into();
60        }
61        self
62    }
63}
64
65fn dimensions() -> dim::Parameters {
66    dim::Parameters {
67        m_large: 5.0,
68        m_text: (3.4, 0.8),
69        frame: 5.0,
70        button_frame: 2.0,
71        button_inner: 3.0,
72        check_box: 24.0,
73        mark: 9.0,
74        handle_len: 8.0,
75        shadow_size: Vec2::splat(6.0),
76        shadow_rel_offset: Vec2::ZERO,
77        ..Default::default()
78    }
79}
80
81const NORMS_FRAME: (f32, f32) = (-0.7, 0.7);
82const NORMS_TRACK: (f32, f32) = (0.0, -0.7);
83const NORMS_SUNK: (f32, f32) = (-0.7, 0.0);
84const NORMS_RAISED: (f32, f32) = (0.0, 0.7);
85
86pub struct DrawHandle<'a, DS: DrawSharedImpl> {
87    draw: DrawIface<'a, DS>,
88    ev: &'a mut EventState,
89    w: &'a mut dim::Window<DS::Draw>,
90    cols: &'a ColorsLinear,
91}
92
93impl<DS: DrawSharedImpl> Theme<DS> for ShadedTheme
94where
95    DS::Draw: DrawRoundedImpl + DrawShadedImpl,
96{
97    type Config = Config;
98    type Window = dim::Window<DS::Draw>;
99
100    type Draw<'a> = DrawHandle<'a, DS>;
101
102    fn config(&self) -> std::borrow::Cow<Self::Config> {
103        <SimpleTheme as Theme<DS>>::config(&self.base)
104    }
105
106    fn apply_config(&mut self, config: &Self::Config) -> TkAction {
107        <SimpleTheme as Theme<DS>>::apply_config(&mut self.base, config)
108    }
109
110    fn init(&mut self, shared: &mut SharedState<DS>) {
111        <SimpleTheme as Theme<DS>>::init(&mut self.base, shared)
112    }
113
114    fn new_window(&self, dpi_factor: f32) -> Self::Window {
115        let fonts = self.base.fonts.as_ref().unwrap().clone();
116        dim::Window::new(&dimensions(), &self.base.config, dpi_factor, fonts)
117    }
118
119    fn update_window(&self, w: &mut Self::Window, dpi_factor: f32) {
120        w.update(&dimensions(), &self.base.config, dpi_factor);
121    }
122
123    fn draw<'a>(
124        &'a self,
125        draw: DrawIface<'a, DS>,
126        ev: &'a mut EventState,
127        w: &'a mut Self::Window,
128    ) -> Self::Draw<'a> {
129        w.anim.update();
130
131        DrawHandle {
132            draw,
133            ev,
134            w,
135            cols: &self.base.cols,
136        }
137    }
138
139    fn clear_color(&self) -> Rgba {
140        <SimpleTheme as Theme<DS>>::clear_color(&self.base)
141    }
142}
143
144impl ThemeControl for ShadedTheme {
145    fn set_font_size(&mut self, pt_size: f32) -> TkAction {
146        self.base.set_font_size(pt_size)
147    }
148
149    fn list_schemes(&self) -> Vec<&str> {
150        self.base.list_schemes()
151    }
152
153    fn set_scheme(&mut self, name: &str) -> TkAction {
154        self.base.set_scheme(name)
155    }
156}
157
158impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS>
159where
160    DS::Draw: DrawRoundedImpl + DrawShadedImpl,
161{
162    // Type-cast to flat_theme's DrawHandle. Should be equivalent to transmute.
163    fn as_flat<'b, 'c>(&'b mut self) -> super::flat_theme::DrawHandle<'c, DS>
164    where
165        'a: 'c,
166        'b: 'c,
167    {
168        super::flat_theme::DrawHandle {
169            draw: self.draw.re(),
170            ev: self.ev,
171            w: self.w,
172            cols: self.cols,
173        }
174    }
175
176    /// Draw an edit box with optional navigation highlight.
177    /// Return the inner rect.
178    fn draw_edit_box(&mut self, outer: Rect, bg_col: Rgba, nav_focus: bool) -> Quad {
179        let outer = Quad::conv(outer);
180        let inner = outer.shrink(self.w.dims.frame as f32);
181        #[cfg(debug_assertions)]
182        {
183            if !inner.a.lt(inner.b) {
184                log::warn!("draw_edit_box: frame too small: {outer:?}");
185            }
186        }
187
188        let outer_col = self.cols.background;
189        let inner_col = if nav_focus { self.cols.accent_soft } else { outer_col };
190        self.draw
191            .shaded_square_frame(outer, inner, NORMS_SUNK, outer_col, inner_col);
192
193        self.draw.rect(inner, bg_col);
194        inner
195    }
196
197    /// Draw a handle (for slider, scroll bar)
198    fn draw_handle(&mut self, rect: Rect, state: InputState) {
199        let outer = Quad::conv(rect);
200        let thickness = outer.size().min_comp() / 2.0;
201        let inner = outer.shrink(thickness);
202        let col = self.cols.accent_soft_state(state);
203        self.draw
204            .shaded_round_frame(outer, inner, NORMS_RAISED, col);
205
206        if let Some(col) = self.cols.nav_region(state) {
207            let outer = outer.shrink(thickness / 4.0);
208            self.draw.rounded_frame(outer, inner, 0.6, col);
209        }
210    }
211}
212
213#[kas::extends(ThemeDraw, base=self.as_flat())]
214impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS>
215where
216    DS::Draw: DrawRoundedImpl + DrawShadedImpl,
217{
218    fn components(&mut self) -> (&dyn ThemeSize, &mut dyn Draw, &mut EventState) {
219        (self.w, &mut self.draw, self.ev)
220    }
221
222    fn new_pass<'b>(
223        &mut self,
224        inner_rect: Rect,
225        offset: Offset,
226        class: PassType,
227        f: Box<dyn FnOnce(&mut dyn ThemeDraw) + 'b>,
228    ) {
229        let mut shadow = Default::default();
230        let mut outer_rect = inner_rect;
231        if class == PassType::Overlay {
232            shadow = Quad::conv(inner_rect);
233            shadow.a += self.w.dims.shadow_a;
234            shadow.b += self.w.dims.shadow_b;
235            let a = Coord::conv_floor(shadow.a);
236            let b = Coord::conv_ceil(shadow.b);
237            outer_rect = Rect::new(a, (b - a).cast());
238        }
239        let mut draw = self.draw.new_pass(outer_rect, offset, class);
240
241        if class == PassType::Overlay {
242            shadow += offset.cast();
243            let inner = Quad::conv(inner_rect + offset);
244            draw.rounded_frame_2col(shadow, inner, Rgba::BLACK, Rgba::TRANSPARENT);
245        }
246
247        let mut handle = DrawHandle {
248            draw,
249            ev: self.ev,
250            w: self.w,
251            cols: self.cols,
252        };
253        f(&mut handle);
254    }
255
256    fn frame(&mut self, id: &WidgetId, rect: Rect, style: FrameStyle, bg: Background) {
257        match style {
258            FrameStyle::Frame => {
259                let outer = Quad::conv(rect);
260                let inner = outer.shrink(self.w.dims.frame as f32);
261                let col = self.cols.background;
262                self.draw.shaded_round_frame(outer, inner, NORMS_FRAME, col);
263            }
264            FrameStyle::Popup => {
265                let outer = Quad::conv(rect);
266                self.draw.rect(outer, self.cols.background);
267            }
268            FrameStyle::MenuEntry => {
269                let state = InputState::new_all(self.ev, id);
270                if let Some(col) = self.cols.menu_entry(state) {
271                    let outer = Quad::conv(rect);
272                    self.draw.rect(outer, col);
273                }
274            }
275            FrameStyle::Button => {
276                let state = InputState::new_all(self.ev, id);
277                let outer = Quad::conv(rect);
278                let frame = self.w.dims.button_frame as f32 + self.w.dims.button_inner as f32;
279                let inner = outer.shrink(frame);
280                let col_bg = self.cols.from_bg(bg, state, true);
281
282                self.draw
283                    .shaded_round_frame(outer, inner, NORMS_RAISED, col_bg);
284                self.draw.rect(inner, col_bg);
285
286                if let Some(col) = self.cols.nav_region(state) {
287                    let outer = outer.shrink(self.w.dims.m_inner as f32);
288                    self.draw.rounded_frame(outer, inner, 0.6, col);
289                }
290            }
291            FrameStyle::EditBox => {
292                let state = InputState::new_except_depress(self.ev, id);
293                let bg_col = self.cols.from_edit_bg(bg, state);
294                self.draw_edit_box(rect, bg_col, state.nav_focus());
295            }
296            style => self.as_flat().frame(id, rect, style, bg),
297        }
298    }
299
300    fn separator(&mut self, rect: Rect) {
301        let outer = Quad::conv(rect);
302        let inner = outer.shrink(outer.size().min_comp() / 2.0);
303        let col = self.cols.background;
304        self.draw.shaded_round_frame(outer, inner, NORMS_TRACK, col);
305    }
306
307    fn check_box(
308        &mut self,
309        id: &WidgetId,
310        rect: Rect,
311        checked: bool,
312        last_change: Option<Instant>,
313    ) {
314        let state = InputState::new_all(self.ev, id);
315        let bg_col = self.cols.from_edit_bg(Default::default(), state);
316        let inner = self.draw_edit_box(rect, bg_col, state.nav_focus());
317
318        self.as_flat()
319            .check_mark(inner, state, checked, last_change);
320    }
321
322    fn radio_box(
323        &mut self,
324        id: &WidgetId,
325        rect: Rect,
326        checked: bool,
327        last_change: Option<Instant>,
328    ) {
329        let state = InputState::new_all(self.ev, id);
330        let anim_fade = 1.0 - self.w.anim.fade_bool(self.draw.draw, checked, last_change);
331
332        let bg_col = self.cols.from_edit_bg(Default::default(), state);
333
334        let inner = self
335            .draw_edit_box(rect, bg_col, state.nav_focus())
336            .shrink(self.w.dims.m_inner as f32);
337
338        if anim_fade < 1.0 {
339            let v = inner.size() * (anim_fade / 2.0);
340            let inner = Quad::from_coords(inner.a + v, inner.b - v);
341            let col = self.cols.check_mark_state(state);
342            self.draw.shaded_circle(inner, NORMS_RAISED, col);
343        }
344    }
345
346    fn scroll_bar(
347        &mut self,
348        id: &WidgetId,
349        id2: &WidgetId,
350        rect: Rect,
351        h_rect: Rect,
352        _: Direction,
353    ) {
354        // track
355        let outer = Quad::conv(rect);
356        let inner = outer.shrink(outer.size().min_comp() / 2.0);
357        let col = self.cols.background;
358        self.draw.shaded_round_frame(outer, inner, NORMS_TRACK, col);
359
360        // handle
361        let state = InputState::new2(self.ev, id, id2);
362        self.draw_handle(h_rect, state);
363    }
364
365    fn slider(&mut self, id: &WidgetId, id2: &WidgetId, rect: Rect, h_rect: Rect, dir: Direction) {
366        // track
367        let mut outer = Quad::conv(rect);
368        outer = match dir.is_horizontal() {
369            true => outer.shrink_vec(Vec2(0.0, outer.size().1 * (3.0 / 8.0))),
370            false => outer.shrink_vec(Vec2(outer.size().0 * (3.0 / 8.0), 0.0)),
371        };
372        let inner = outer.shrink(outer.size().min_comp() / 2.0);
373        let col = self.cols.background;
374        self.draw.shaded_round_frame(outer, inner, NORMS_TRACK, col);
375
376        // handle
377        let state = InputState::new2(self.ev, id, id2);
378        self.draw_handle(h_rect, state);
379    }
380
381    fn progress_bar(&mut self, _: &WidgetId, rect: Rect, dir: Direction, value: f32) {
382        let mut outer = Quad::conv(rect);
383        let inner = outer.shrink(outer.size().min_comp() / 2.0);
384        let col = self.cols.frame;
385        self.draw.shaded_round_frame(outer, inner, NORMS_TRACK, col);
386
387        if dir.is_horizontal() {
388            outer.b.0 = outer.a.0 + value * (outer.b.0 - outer.a.0);
389        } else {
390            outer.b.1 = outer.a.1 + value * (outer.b.1 - outer.a.1);
391        }
392        let thickness = outer.size().min_comp() / 2.0;
393        let inner = outer.shrink(thickness);
394        let col = self.cols.accent_soft;
395        self.draw
396            .shaded_round_frame(outer, inner, NORMS_RAISED, col);
397    }
398}