Skip to main content

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