kas_theme/
simple_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//! Simple theme
7
8use linear_map::LinearMap;
9use std::f32;
10use std::ops::Range;
11use std::rc::Rc;
12use std::time::Instant;
13
14use crate::{dim, ColorsLinear, Config, InputState, Theme};
15use kas::cast::traits::*;
16use kas::dir::{Direction, Directional};
17use kas::draw::{color::Rgba, *};
18use kas::event::EventState;
19use kas::geom::*;
20use kas::text::{fonts, Effect, TextApi, TextDisplay};
21use kas::theme::{Background, FrameStyle, MarkStyle, TextClass};
22use kas::theme::{ThemeControl, ThemeDraw, ThemeSize};
23use kas::{TkAction, WidgetId};
24
25/// A simple theme
26#[derive(Clone, Debug)]
27pub struct SimpleTheme {
28    pub(crate) config: Config,
29    pub(crate) cols: ColorsLinear,
30    dims: dim::Parameters,
31    pub(crate) fonts: Option<Rc<LinearMap<TextClass, fonts::FontId>>>,
32}
33
34impl Default for SimpleTheme {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl SimpleTheme {
41    /// Construct
42    #[inline]
43    pub fn new() -> Self {
44        let cols = ColorsLinear::default();
45        SimpleTheme {
46            config: Default::default(),
47            cols,
48            dims: Default::default(),
49            fonts: None,
50        }
51    }
52
53    /// Set font size
54    ///
55    /// Units: Points per Em (standard unit of font size)
56    #[inline]
57    #[must_use]
58    pub fn with_font_size(mut self, pt_size: f32) -> Self {
59        self.config.set_font_size(pt_size);
60        self
61    }
62
63    /// Set the colour scheme
64    ///
65    /// If no scheme by this name is found the scheme is left unchanged.
66    #[inline]
67    #[must_use]
68    pub fn with_colours(mut self, name: &str) -> Self {
69        if let Some(scheme) = self.config.get_color_scheme(name) {
70            self.config.set_active_scheme(name);
71            let _ = self.set_colors(scheme.into());
72        }
73        self
74    }
75
76    pub fn set_colors(&mut self, cols: ColorsLinear) -> TkAction {
77        self.cols = cols;
78        TkAction::REDRAW
79    }
80}
81
82pub struct DrawHandle<'a, DS: DrawSharedImpl> {
83    pub(crate) draw: DrawIface<'a, DS>,
84    pub(crate) ev: &'a mut EventState,
85    pub(crate) w: &'a mut dim::Window<DS::Draw>,
86    pub(crate) cols: &'a ColorsLinear,
87}
88
89impl<DS: DrawSharedImpl> Theme<DS> for SimpleTheme
90where
91    DS::Draw: DrawRoundedImpl,
92{
93    type Config = Config;
94    type Window = dim::Window<DS::Draw>;
95
96    type Draw<'a> = DrawHandle<'a, DS>;
97
98    fn config(&self) -> std::borrow::Cow<Self::Config> {
99        std::borrow::Cow::Borrowed(&self.config)
100    }
101
102    fn apply_config(&mut self, config: &Self::Config) -> TkAction {
103        let mut action = self.config.apply_config(config);
104        if let Some(scheme) = self.config.get_active_scheme() {
105            action |= self.set_colors(scheme.into());
106        }
107        action
108    }
109
110    fn init(&mut self, _shared: &mut SharedState<DS>) {
111        let fonts = fonts::fonts();
112        if let Err(e) = fonts.select_default() {
113            panic!("Error loading font: {e}");
114        }
115        self.fonts = Some(Rc::new(
116            self.config
117                .iter_fonts()
118                .filter_map(|(c, s)| fonts.select_font(s).ok().map(|id| (*c, id)))
119                .collect(),
120        ));
121    }
122
123    fn new_window(&self, dpi_factor: f32) -> Self::Window {
124        let fonts = self.fonts.as_ref().unwrap().clone();
125        dim::Window::new(&self.dims, &self.config, dpi_factor, fonts)
126    }
127
128    fn update_window(&self, w: &mut Self::Window, dpi_factor: f32) {
129        w.update(&self.dims, &self.config, dpi_factor);
130    }
131
132    fn draw<'a>(
133        &'a self,
134        draw: DrawIface<'a, DS>,
135        ev: &'a mut EventState,
136        w: &'a mut Self::Window,
137    ) -> Self::Draw<'a> {
138        w.anim.update();
139
140        DrawHandle {
141            draw,
142            ev,
143            w,
144            cols: &self.cols,
145        }
146    }
147
148    fn clear_color(&self) -> Rgba {
149        self.cols.background
150    }
151}
152
153impl ThemeControl for SimpleTheme {
154    fn set_font_size(&mut self, pt_size: f32) -> TkAction {
155        self.config.set_font_size(pt_size);
156        TkAction::RESIZE | TkAction::THEME_UPDATE
157    }
158
159    fn list_schemes(&self) -> Vec<&str> {
160        self.config
161            .color_schemes_iter()
162            .map(|(name, _)| name)
163            .collect()
164    }
165
166    fn set_scheme(&mut self, name: &str) -> TkAction {
167        if name != self.config.active_scheme() {
168            if let Some(scheme) = self.config.get_color_scheme(name) {
169                self.config.set_active_scheme(name);
170                return self.set_colors(scheme.into());
171            }
172        }
173        TkAction::empty()
174    }
175}
176
177impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS>
178where
179    DS::Draw: DrawRoundedImpl,
180{
181    pub fn button_frame(
182        &mut self,
183        outer: Quad,
184        col_frame: Rgba,
185        col_bg: Rgba,
186        _: InputState,
187    ) -> Quad {
188        let inner = outer.shrink(self.w.dims.button_frame as f32);
189        #[cfg(debug_assertions)]
190        {
191            if !inner.a.lt(inner.b) {
192                log::warn!("button_frame: frame too small: {outer:?}");
193            }
194        }
195
196        let bgr = outer.shrink(self.w.dims.button_frame as f32);
197        self.draw.rect(bgr, col_bg);
198
199        self.draw.frame(outer, inner, col_frame);
200        inner
201    }
202
203    pub fn edit_box(&mut self, id: &WidgetId, outer: Quad, bg: Background) {
204        let state = InputState::new_except_depress(self.ev, id);
205        let col_bg = self.cols.from_edit_bg(bg, state);
206        if col_bg != self.cols.background {
207            let inner = outer.shrink(self.w.dims.button_frame as f32);
208            self.draw.rect(inner, col_bg);
209        }
210
211        let inner = outer.shrink(self.w.dims.button_frame as f32);
212        self.draw.frame(outer, inner, self.cols.frame);
213
214        if !state.disabled() && !self.cols.is_dark && (state.nav_focus() || state.hover()) {
215            let mut line = outer;
216            line.a.1 = line.b.1 - self.w.dims.button_frame as f32;
217            let col = if state.nav_focus() {
218                self.cols.nav_focus
219            } else {
220                self.cols.text
221            };
222            self.draw.rect(line, col);
223        }
224    }
225}
226
227impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS>
228where
229    DS::Draw: DrawRoundedImpl,
230{
231    fn components(&mut self) -> (&dyn ThemeSize, &mut dyn Draw, &mut EventState) {
232        (self.w, &mut self.draw, self.ev)
233    }
234
235    fn new_pass<'b>(
236        &mut self,
237        inner_rect: Rect,
238        offset: Offset,
239        class: PassType,
240        f: Box<dyn FnOnce(&mut dyn ThemeDraw) + 'b>,
241    ) {
242        let draw = self.draw.new_pass(inner_rect, offset, class);
243        let mut handle = DrawHandle {
244            draw,
245            ev: self.ev,
246            w: self.w,
247            cols: self.cols,
248        };
249        f(&mut handle);
250    }
251
252    fn get_clip_rect(&mut self) -> Rect {
253        self.draw.get_clip_rect()
254    }
255
256    fn frame(&mut self, id: &WidgetId, rect: Rect, style: FrameStyle, bg: Background) {
257        let outer = Quad::conv(rect);
258        match style {
259            FrameStyle::Frame => {
260                let inner = outer.shrink(self.w.dims.frame as f32);
261                self.draw.frame(outer, inner, self.cols.frame);
262            }
263            FrameStyle::Popup => {
264                // We cheat here by using zero-sized popup-frame, but assuming that contents are
265                // all a MenuEntry, and drawing into this space. This might look wrong if other
266                // widgets are used in the popup.
267                let size = self.w.dims.menu_frame as f32;
268                let inner = outer.shrink(size);
269                self.draw.frame(outer, inner, self.cols.frame);
270                self.draw.rect(inner, self.cols.background);
271            }
272            FrameStyle::MenuEntry => {
273                let state = InputState::new_all(self.ev, id);
274                if let Some(col) = self.cols.menu_entry(state) {
275                    self.draw.rect(outer, col);
276                }
277            }
278            FrameStyle::NavFocus => {
279                let state = InputState::new_all(self.ev, id);
280                if let Some(col) = self.cols.nav_region(state) {
281                    let inner = outer.shrink(self.w.dims.m_inner as f32);
282                    self.draw.frame(outer, inner, col);
283                }
284            }
285            FrameStyle::Button => {
286                let state = InputState::new_all(self.ev, id);
287                let outer = Quad::conv(rect);
288
289                let col_bg = self.cols.from_bg(bg, state, false);
290                let col_frame = self.cols.nav_region(state).unwrap_or(self.cols.frame);
291                self.button_frame(outer, col_frame, col_bg, state);
292            }
293            FrameStyle::EditBox => self.edit_box(id, outer, bg),
294        }
295    }
296
297    fn separator(&mut self, rect: Rect) {
298        let outer = Quad::conv(rect);
299        self.draw.rect(outer, self.cols.frame);
300    }
301
302    fn selection_box(&mut self, rect: Rect) {
303        let inner = Quad::conv(rect);
304        let outer = inner.grow(self.w.dims.m_inner.into());
305        // TODO: this should use its own colour and a stippled pattern
306        let col = self.cols.text_sel_bg;
307        self.draw.frame(outer, inner, col);
308    }
309
310    fn text(&mut self, id: &WidgetId, rect: Rect, text: &TextDisplay, _: TextClass) {
311        let col = if self.ev.is_disabled(id) {
312            self.cols.text_disabled
313        } else {
314            self.cols.text
315        };
316        self.draw.text(rect, text, col);
317    }
318
319    fn text_effects(&mut self, id: &WidgetId, rect: Rect, text: &dyn TextApi, class: TextClass) {
320        let col = if self.ev.is_disabled(id) {
321            self.cols.text_disabled
322        } else {
323            self.cols.text
324        };
325        if class.is_accel() && !self.ev.show_accel_labels() {
326            self.draw.text(rect, text.display(), col);
327        } else {
328            self.draw
329                .text_effects(rect, text.display(), col, text.effect_tokens());
330        }
331    }
332
333    fn text_selected_range(
334        &mut self,
335        id: &WidgetId,
336        rect: Rect,
337        text: &TextDisplay,
338        range: Range<usize>,
339        _: TextClass,
340    ) {
341        let col = if self.ev.is_disabled(id) {
342            self.cols.text_disabled
343        } else {
344            self.cols.text
345        };
346        let sel_col = self.cols.text_over(self.cols.text_sel_bg);
347
348        // Draw background:
349        let result = text.highlight_range(range.clone(), &mut |p1, p2| {
350            let q = Quad::conv(rect);
351            let p1 = Vec2::from(p1);
352            let p2 = Vec2::from(p2);
353            if let Some(quad) = Quad::from_coords(q.a + p1, q.a + p2).intersection(&q) {
354                self.draw.rect(quad, self.cols.text_sel_bg);
355            }
356        });
357        if let Err(e) = result {
358            log::error!("text_selected_range: text.highlight_range() -> {e}");
359        }
360
361        let effects = [
362            Effect {
363                start: 0,
364                flags: Default::default(),
365                aux: col,
366            },
367            Effect {
368                start: range.start.cast(),
369                flags: Default::default(),
370                aux: sel_col,
371            },
372            Effect {
373                start: range.end.cast(),
374                flags: Default::default(),
375                aux: col,
376            },
377        ];
378        self.draw.text_effects_rgba(rect, text, &effects);
379    }
380
381    fn text_cursor(
382        &mut self,
383        id: &WidgetId,
384        rect: Rect,
385        text: &TextDisplay,
386        _: TextClass,
387        byte: usize,
388    ) {
389        if self.ev.window_has_focus() && !self.w.anim.text_cursor(self.draw.draw, id, byte) {
390            return;
391        }
392
393        let width = self.w.dims.mark_line;
394        let pos = Vec2::conv(rect.pos);
395        let p10max = pos.0 + f32::conv(rect.size.0) - width;
396
397        let mut col = self.cols.nav_focus;
398        for cursor in text.text_glyph_pos(byte).iter_mut().flatten().rev() {
399            let mut p1 = pos + Vec2::from(cursor.pos);
400            p1.0 = p1.0.min(p10max);
401            let mut p2 = p1;
402            p1.1 -= cursor.ascent;
403            p2.1 -= cursor.descent;
404            p2.0 += width;
405            let quad = Quad::from_coords(p1, p2);
406            self.draw.rect(quad, col);
407
408            if cursor.embedding_level() > 0 {
409                // Add a hat to indicate directionality.
410                let height = width;
411                let quad = if cursor.is_ltr() {
412                    Quad::from_coords(Vec2(p2.0, p1.1), Vec2(p2.0 + width, p1.1 + height))
413                } else {
414                    Quad::from_coords(Vec2(p1.0 - width, p1.1), Vec2(p1.0, p1.1 + height))
415                };
416                self.draw.rect(quad, col);
417            }
418            // hack to make secondary marker grey:
419            col = col.average();
420        }
421    }
422
423    fn check_box(&mut self, id: &WidgetId, rect: Rect, checked: bool, _: Option<Instant>) {
424        let state = InputState::new_all(self.ev, id);
425        let outer = Quad::conv(rect);
426
427        let col_frame = self.cols.nav_region(state).unwrap_or(self.cols.frame);
428        let col_bg = self.cols.from_edit_bg(Default::default(), state);
429        let inner = self.button_frame(outer, col_frame, col_bg, state);
430
431        if checked {
432            let inner = inner.shrink(self.w.dims.m_inner as f32);
433            let col = self.cols.check_mark_state(state);
434            self.draw.rect(inner, col);
435        }
436    }
437
438    fn radio_box(
439        &mut self,
440        id: &WidgetId,
441        rect: Rect,
442        checked: bool,
443        last_change: Option<Instant>,
444    ) {
445        self.check_box(id, rect, checked, last_change);
446    }
447
448    fn mark(&mut self, id: &WidgetId, rect: Rect, style: MarkStyle) {
449        let col = if self.ev.is_disabled(id) {
450            self.cols.text_disabled
451        } else if self.ev.is_hovered(id) {
452            self.cols.accent
453        } else {
454            self.cols.text
455        };
456
457        match style {
458            MarkStyle::Point(dir) => {
459                let size = match dir.is_horizontal() {
460                    true => Size(self.w.dims.mark / 2, self.w.dims.mark),
461                    false => Size(self.w.dims.mark, self.w.dims.mark / 2),
462                };
463                let offset = Offset::conv((rect.size - size) / 2);
464                let q = Quad::conv(Rect::new(rect.pos + offset, size));
465
466                let (p1, p2, p3);
467                if dir.is_horizontal() {
468                    let (mut x1, mut x2) = (q.a.0, q.b.0);
469                    if dir.is_reversed() {
470                        std::mem::swap(&mut x1, &mut x2);
471                    }
472                    p1 = Vec2(x1, q.a.1);
473                    p2 = Vec2(x2, 0.5 * (q.a.1 + q.b.1));
474                    p3 = Vec2(x1, q.b.1);
475                } else {
476                    let (mut y1, mut y2) = (q.a.1, q.b.1);
477                    if dir.is_reversed() {
478                        std::mem::swap(&mut y1, &mut y2);
479                    }
480                    p1 = Vec2(q.a.0, y1);
481                    p2 = Vec2(0.5 * (q.a.0 + q.b.0), y2);
482                    p3 = Vec2(q.b.0, y1);
483                };
484
485                let f = 0.5 * self.w.dims.mark_line;
486                self.draw.rounded_line(p1, p2, f, col);
487                self.draw.rounded_line(p2, p3, f, col);
488            }
489        }
490    }
491
492    fn scroll_bar(
493        &mut self,
494        id: &WidgetId,
495        id2: &WidgetId,
496        rect: Rect,
497        h_rect: Rect,
498        _: Direction,
499    ) {
500        let track = Quad::conv(rect);
501        self.draw.rect(track, self.cols.frame);
502
503        let handle = Quad::conv(h_rect);
504        let state = InputState::new2(self.ev, id, id2);
505        let col = self.cols.accent_soft_state(state);
506        self.draw.rect(handle, col);
507    }
508
509    fn slider(&mut self, id: &WidgetId, id2: &WidgetId, rect: Rect, h_rect: Rect, _: Direction) {
510        let track = Quad::conv(rect);
511        self.draw.rect(track, self.cols.frame);
512
513        let handle = Quad::conv(h_rect);
514        let state = InputState::new2(self.ev, id, id2);
515        let col = self.cols.accent_soft_state(state);
516        self.draw.rect(handle, col);
517    }
518
519    fn progress_bar(&mut self, _: &WidgetId, rect: Rect, dir: Direction, value: f32) {
520        let mut outer = Quad::conv(rect);
521        self.draw.rect(outer, self.cols.frame);
522
523        if dir.is_horizontal() {
524            outer.b.0 = outer.a.0 + value * (outer.b.0 - outer.a.0);
525        } else {
526            outer.b.1 = outer.a.1 + value * (outer.b.1 - outer.a.1);
527        }
528        self.draw.rect(outer, self.cols.accent);
529    }
530
531    fn image(&mut self, id: ImageId, rect: Rect) {
532        self.draw.image(id, rect.cast());
533    }
534}