kas_core/theme/
draw.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//! Widget-facing high-level draw API
7
8use winit::keyboard::Key;
9
10use super::{FrameStyle, MarkStyle, SelectionStyle, SizeCx, Text, ThemeSize};
11use crate::dir::Direction;
12use crate::draw::color::{ParseError, Rgb};
13use crate::draw::{Draw, DrawIface, DrawShared, DrawSharedImpl, ImageId, PassType};
14#[allow(unused)] use crate::event::{Command, Event};
15use crate::event::{ConfigCx, EventState};
16use crate::geom::{Coord, Offset, Rect};
17use crate::text::{Effect, TextDisplay, format::FormattableText};
18use crate::{Id, Tile, autoimpl};
19#[allow(unused)] use crate::{Layout, theme::TextClass};
20use std::ops::Range;
21use std::time::Instant;
22
23/// Optional background colour
24#[derive(Copy, Clone, Debug, Default, PartialEq)]
25pub enum Background {
26    /// Use theme/feature's default
27    #[default]
28    Default,
29    /// Error state
30    Error,
31    /// A given color
32    Rgb(Rgb),
33}
34
35impl From<Rgb> for Background {
36    #[inline]
37    fn from(color: Rgb) -> Self {
38        Background::Rgb(color)
39    }
40}
41
42#[derive(Copy, Clone, Debug, thiserror::Error)]
43pub enum BackgroundParseError {
44    /// No `#` prefix
45    ///
46    /// NOTE: this exists to allow the possibility of supporting new exprs like
47    /// "Default" or "Error".
48    #[error("Unknown: no `#` prefix")]
49    Unknown,
50    /// Invalid hex
51    #[error("invalid hex sequence")]
52    InvalidRgb(#[from] ParseError),
53}
54
55impl std::str::FromStr for Background {
56    type Err = BackgroundParseError;
57
58    #[inline]
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        if s.starts_with("#") {
61            Rgb::from_str(s).map(|c| c.into()).map_err(|e| e.into())
62        } else {
63            Err(BackgroundParseError::Unknown)
64        }
65    }
66}
67
68/// Draw interface
69///
70/// This interface is provided to widgets in [`crate::Layout::draw`].
71/// Lower-level interfaces may be accessed through [`Self::draw_device`].
72///
73/// `DrawCx` is not a `Copy` or `Clone` type; instead it may be "reborrowed"
74/// via [`Self::re`].
75///
76/// -   `draw.check_box(&*self, self.state);` — note `&*self` to convert from to
77///     `&W` from `&mut W`, since the latter would cause borrow conflicts
78#[autoimpl(Debug ignore self.h)]
79pub struct DrawCx<'a> {
80    h: &'a mut dyn ThemeDraw,
81    id: Id,
82}
83
84impl<'a> DrawCx<'a> {
85    /// Reborrow with a new lifetime
86    ///
87    /// Rust allows references like `&T` or `&mut T` to be "reborrowed" through
88    /// coercion: essentially, the pointer is copied under a new, shorter, lifetime.
89    /// Until rfcs#1403 lands, reborrows on user types require a method call.
90    #[inline(always)]
91    pub fn re<'b>(&'b mut self) -> DrawCx<'b>
92    where
93        'a: 'b,
94    {
95        DrawCx {
96            h: self.h,
97            id: self.id.clone(),
98        }
99    }
100
101    /// Construct from a [`DrawCx`] and [`EventState`]
102    #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
103    #[cfg_attr(docsrs, doc(cfg(internal_doc)))]
104    pub(crate) fn new(h: &'a mut dyn ThemeDraw, id: Id) -> Self {
105        DrawCx { h, id }
106    }
107
108    /// Set the identity of the current widget
109    ///
110    /// This struct tracks the [`Id`] of the calling widget to allow evaluation
111    /// of widget state (e.g. is disabled, is under the mouse, has key focus).
112    /// Usually you don't need to worry about this since the `#[widget]` macro
113    /// injects a call to this method at the start of [`Layout::draw`].
114    pub fn set_id(&mut self, id: Id) {
115        self.id = id;
116    }
117
118    /// Access event-management state
119    pub fn ev_state(&mut self) -> &mut EventState {
120        self.h.components().2
121    }
122
123    /// Access a [`SizeCx`]
124    pub fn size_cx(&mut self) -> SizeCx<'_> {
125        SizeCx::new(self.h.components().0)
126    }
127
128    /// Access a [`ConfigCx`]
129    pub fn config_cx<F: FnOnce(&mut ConfigCx) -> T, T>(&mut self, f: F) -> T {
130        let (sh, _, ev) = self.h.components();
131        let mut cx = ConfigCx::new(sh, ev);
132        f(&mut cx)
133    }
134
135    /// Access a [`DrawShared`]
136    pub fn draw_shared(&mut self) -> &mut dyn DrawShared {
137        self.h.components().1.shared()
138    }
139
140    /// Access the low-level draw device
141    ///
142    /// Note: this drawing API is modular, with limited functionality in the
143    /// base trait [`Draw`]. To access further functionality, it is necessary
144    /// to downcast with [`crate::draw::DrawIface::downcast_from`].
145    pub fn draw_device(&mut self) -> &mut dyn Draw {
146        self.h.components().1
147    }
148
149    /// Access the low-level draw device (implementation type)
150    ///
151    /// The implementing type must be specified. See [`DrawIface::downcast_from`].
152    pub fn draw_iface<DS: DrawSharedImpl>(&mut self) -> Option<DrawIface<'_, DS>> {
153        DrawIface::downcast_from(self.draw_device())
154    }
155
156    /// Draw to a new pass
157    ///
158    /// Adds a new draw pass for purposes of enforcing draw order. Content of
159    /// the new pass will be drawn after content in the parent pass.
160    ///
161    /// Warning: the number of passes used can have a substantial performance
162    /// impact, potentially more on GPU communication than CPU usage.
163    pub fn with_pass<F: FnOnce(DrawCx)>(&mut self, f: F) {
164        let clip_rect = self.h.get_clip_rect();
165        let id = self.id.clone();
166        self.h.new_pass(
167            clip_rect,
168            Offset::ZERO,
169            PassType::Clip,
170            Box::new(|h| f(DrawCx { h, id })),
171        );
172    }
173
174    /// Draw to a new pass with clipping and offset (e.g. for scrolling)
175    ///
176    /// Adds a new draw pass of type [`PassType::Clip`], with draw operations
177    /// clipped to `rect` and translated by `offset.
178    ///
179    /// Warning: the number of passes used can have a substantial performance
180    /// impact, potentially more on GPU communication than CPU usage.
181    pub fn with_clip_region<F: FnOnce(DrawCx)>(&mut self, rect: Rect, offset: Offset, f: F) {
182        let id = self.id.clone();
183        self.h.new_pass(
184            rect,
185            offset,
186            PassType::Clip,
187            Box::new(|h| f(DrawCx { h, id })),
188        );
189    }
190
191    /// Draw to a new pass as an overlay (e.g. for pop-up menus)
192    ///
193    /// Adds a new draw pass of type [`PassType::Overlay`], with draw operations
194    /// clipped to `rect`.
195    ///
196    /// The theme is permitted to enlarge the `rect` for the purpose of drawing
197    /// a frame or shadow around this overlay, thus the
198    /// [`Self::get_clip_rect`] may be larger than expected.
199    ///
200    /// Warning: the number of passes used can have a substantial performance
201    /// impact, potentially more on GPU communication than CPU usage.
202    pub fn with_overlay<F: FnOnce(DrawCx)>(&mut self, rect: Rect, offset: Offset, f: F) {
203        let id = self.id.clone();
204        self.h.new_pass(
205            rect,
206            offset,
207            PassType::Overlay,
208            Box::new(|h| f(DrawCx { h, id })),
209        );
210    }
211
212    /// Target area for drawing
213    ///
214    /// Drawing is restricted to this [`Rect`], which may be the whole window, a
215    /// [clip region](Self::with_clip_region) or an
216    /// [overlay](Self::with_overlay). This may be used to cull hidden
217    /// items from lists inside a scrollable view.
218    pub fn get_clip_rect(&mut self) -> Rect {
219        self.h.get_clip_rect()
220    }
221
222    /// Register widget `id` as handler of an access `key`
223    ///
224    /// An *access key* (also known as mnemonic) is a shortcut key able to
225    /// directly open menus, activate buttons, etc. Usually this requires that
226    /// the <kbd>Alt</kbd> is held, though
227    /// [alt-bypass mode](crate::window::Window::with_alt_bypass) is available.
228    ///
229    /// The widget `id` is bound to the given `key`, if available. When the
230    /// access key is pressed (assuming that this binding succeeds), widget `id`
231    /// will receive navigation focus (if supported; otherwise an ancestor may
232    /// receive focus) and is sent [`Command::Activate`] (likewise, an ancestor
233    /// may handle this if widget `id` does not).
234    ///
235    /// If multiple widgets attempt to register themselves as handlers of the
236    /// same `key`, then only the first succeeds.
237    ///
238    /// Returns `true` when the key should be underlined.
239    pub fn access_key(&mut self, id: &Id, key: &Key) -> bool {
240        self.ev_state().add_access_key_binding(id, key)
241    }
242
243    /// Draw a frame inside the given `rect`
244    ///
245    /// The frame dimensions are given by [`SizeCx::frame`].
246    pub fn frame(&mut self, rect: Rect, style: FrameStyle, bg: Background) {
247        self.h.frame(&self.id, rect, style, bg)
248    }
249
250    /// Draw a separator in the given `rect`
251    pub fn separator(&mut self, rect: Rect) {
252        self.h.separator(rect);
253    }
254
255    /// Draw a selection highlight / frame
256    ///
257    /// Adjusts the background color and/or draws a line around the given rect.
258    /// In the latter case, a margin of size [`SizeCx::inner_margins`] around
259    /// `rect` is expected.
260    pub fn selection(&mut self, rect: Rect, style: SelectionStyle) {
261        self.h.selection(rect, style);
262    }
263
264    /// Draw text with effects
265    ///
266    /// Text is drawn from `rect.pos` and clipped to `rect`.
267    ///
268    /// This method supports a number of text effects: bold, emphasis, text
269    /// size, underline and strikethrough.
270    ///
271    /// [`ConfigCx::text_configure`] should be called prior to this method to
272    /// select a font, font size and wrap options (based on the [`TextClass`]).
273    pub fn text<T: FormattableText>(&mut self, rect: Rect, text: &Text<T>) {
274        self.text_pos(rect.pos, rect, text);
275    }
276
277    /// Draw text with effects and an offset
278    ///
279    /// Text is drawn from `pos` and clipped to `rect`.
280    ///
281    /// This method supports a number of text effects: bold, emphasis, text
282    /// size, underline and strikethrough.
283    ///
284    /// [`ConfigCx::text_configure`] should be called prior to this method to
285    /// select a font, font size and wrap options (based on the [`TextClass`]).
286    pub fn text_pos<T: FormattableText>(&mut self, pos: Coord, rect: Rect, text: &Text<T>) {
287        let effects = text.effect_tokens();
288        self.text_with_effects(pos, rect, text, effects);
289    }
290
291    /// Draw text with a given effect list
292    ///
293    /// Text is drawn from `pos` and clipped to `rect`.
294    ///
295    /// This method supports a number of text effects: bold, emphasis, text
296    /// size, underline and strikethrough.
297    ///
298    /// This method is similar to [`Self::text_pos`] except that an effect list
299    /// is passed explicitly instead of inferred from `text`.
300    pub fn text_with_effects<T: FormattableText>(
301        &mut self,
302        pos: Coord,
303        rect: Rect,
304        text: &Text<T>,
305        effects: &[Effect],
306    ) {
307        if let Ok(display) = text.display() {
308            if effects.is_empty() {
309                // Use the faster and simpler implementation when we don't have effects
310                self.h.text(&self.id, pos, rect, display);
311            } else {
312                self.h.text_effects(&self.id, pos, rect, display, effects);
313            }
314        }
315    }
316
317    /// Draw some text using the standard font, with a subset selected
318    ///
319    /// Other than visually highlighting the selection, this method behaves
320    /// identically to [`Self::text`]. It is likely to be replaced in the
321    /// future by a higher-level API.
322    pub fn text_selected<T: FormattableText>(
323        &mut self,
324        pos: Coord,
325        rect: Rect,
326        text: &Text<T>,
327        range: Range<usize>,
328    ) {
329        if range.is_empty() {
330            return self.text_pos(pos, rect, text);
331        }
332
333        let Ok(display) = text.display() else {
334            return;
335        };
336
337        self.h
338            .text_selected_range(&self.id, pos, rect, display, range);
339    }
340
341    /// Draw an edit marker at the given `byte` index on this `text`
342    ///
343    /// The text cursor is draw from `rect.pos` and clipped to `rect`.
344    ///
345    /// [`ConfigCx::text_configure`] should be called prior to this method to
346    /// select a font, font size and wrap options (based on the [`TextClass`]).
347    pub fn text_cursor<T: FormattableText>(
348        &mut self,
349        pos: Coord,
350        rect: Rect,
351        text: &Text<T>,
352        byte: usize,
353    ) {
354        if let Ok(text) = text.display() {
355            self.h.text_cursor(&self.id, pos, rect, text, byte);
356        }
357    }
358
359    /// Draw UI element: check box (without label)
360    ///
361    /// The check box is a small visual element, typically a distinctive square
362    /// box with or without a "check" selection mark.
363    ///
364    /// The theme may animate transitions. To achieve this, `last_change` should be
365    /// the time of the last state change caused by the user, or none when the
366    /// last state change was programmatic.
367    pub fn check_box(&mut self, rect: Rect, checked: bool, last_change: Option<Instant>) {
368        self.h.check_box(&self.id, rect, checked, last_change);
369    }
370
371    /// Draw UI element: radio box (without label)
372    ///
373    /// The radio box is a small visual element, typically a disinctive
374    /// circular box with or without a "radio" selection mark.
375    ///
376    /// The theme may animate transitions. To achieve this, `last_change` should be
377    /// the time of the last state change caused by the user, or none when the
378    /// last state change was programmatic.
379    pub fn radio_box(&mut self, rect: Rect, checked: bool, last_change: Option<Instant>) {
380        self.h.radio_box(&self.id, rect, checked, last_change);
381    }
382
383    /// Draw UI element: mark
384    pub fn mark(&mut self, rect: Rect, style: MarkStyle) {
385        self.h.mark(&self.id, rect, style);
386    }
387
388    /// Draw UI element: scroll bar
389    pub fn scroll_bar<W: Tile>(&mut self, track_rect: Rect, grip: &W, dir: Direction) {
390        self.h
391            .scroll_bar(&self.id, grip.id_ref(), track_rect, grip.rect(), dir);
392    }
393
394    /// Draw UI element: slider
395    pub fn slider<W: Tile>(&mut self, track_rect: Rect, grip: &W, dir: Direction) {
396        self.h
397            .slider(&self.id, grip.id_ref(), track_rect, grip.rect(), dir);
398    }
399
400    /// Draw UI element: progress bar
401    ///
402    /// -   `rect`: area of whole widget
403    /// -   `dir`: direction of progress bar
404    /// -   `state`: highlighting information
405    /// -   `value`: progress value, between 0.0 and 1.0
406    pub fn progress_bar(&mut self, rect: Rect, dir: Direction, value: f32) {
407        self.h.progress_bar(&self.id, rect, dir, value);
408    }
409
410    /// Draw an image
411    pub fn image(&mut self, rect: Rect, id: ImageId) {
412        self.h.image(id, rect);
413    }
414}
415
416/// Theme drawing implementation
417///
418/// # Theme extension
419///
420/// Most themes will not want to implement *everything*, but rather derive
421/// not-explicitly-implemented methods from a base theme. This may be achieved
422/// with the [`kas::extends`](crate::extends) macro:
423/// ```ignore
424/// #[extends(ThemeDraw, base = self.base())]
425/// impl ThemeDraw {
426///     // only implement some methods here
427/// }
428/// ```
429/// Note: [`Self::components`] must be implemented
430/// explicitly since this method returns references.
431///
432/// If Rust had stable specialization + GATs + negative trait bounds we could
433/// allow theme extension without macros as follows.
434/// <details>
435///
436/// ```ignore
437/// #![feature(generic_associated_types)]
438/// #![feature(specialization)]
439/// # use kas_core::geom::Rect;
440/// # use kas_core::theme::ThemeDraw;
441/// /// Provides a default implementation of each theme method over a base theme
442/// pub trait ThemeDrawExtends: ThemeDraw {
443///     /// Type of base implementation
444///     type Base<'a>: ThemeDraw where Self: 'a;
445///
446///     /// Access the base theme
447///     fn base<'a>(&'a mut self) -> Self::Base<'a>;
448/// }
449///
450/// // Note: we may need negative trait bounds here to avoid conflict with impl for Box<H>
451/// impl<D: ThemeDrawExtends> ThemeDraw for D {
452///     default fn get_clip_rect(&mut self) -> Rect {
453///         self.base().get_clip_rect()
454///     }
455///
456///     // And so on for other methods...
457/// }
458/// ```
459/// </details>
460#[autoimpl(for<H: trait + ?Sized> Box<H>)]
461pub trait ThemeDraw {
462    /// Access components: [`ThemeSize`], [`Draw`], [`EventState`]
463    fn components(&mut self) -> (&dyn ThemeSize, &mut dyn Draw, &mut EventState);
464
465    /// Construct a new pass
466    fn new_pass<'a>(
467        &mut self,
468        rect: Rect,
469        offset: Offset,
470        class: PassType,
471        f: Box<dyn FnOnce(&mut dyn ThemeDraw) + 'a>,
472    );
473
474    /// Target area for drawing
475    ///
476    /// Drawing is restricted to this [`Rect`]. Affected by [`Self::new_pass`].
477    /// This may be used to cull hidden items from lists inside a scrollable view.
478    fn get_clip_rect(&mut self) -> Rect;
479
480    /// Draw [`EventState`] overlay
481    fn event_state_overlay(&mut self);
482
483    /// Draw a frame inside the given `rect`
484    ///
485    /// The frame dimensions are given by [`ThemeSize::frame`].
486    fn frame(&mut self, id: &Id, rect: Rect, style: FrameStyle, bg: Background);
487
488    /// Draw a separator in the given `rect`
489    fn separator(&mut self, rect: Rect);
490
491    /// Draw a selection highlight / frame
492    fn selection(&mut self, rect: Rect, style: SelectionStyle);
493
494    /// Draw text
495    ///
496    /// [`ConfigCx::text_configure`] should be called prior to this method to
497    /// select a font, font size and wrap options (based on the [`TextClass`]).
498    fn text(&mut self, id: &Id, pos: Coord, rect: Rect, text: &TextDisplay);
499
500    /// Draw text with effects
501    ///
502    /// [`ThemeDraw::text`] already supports *font* effects: bold,
503    /// emphasis, text size. In addition, this method supports underline and
504    /// strikethrough effects.
505    ///
506    /// If `effects` is empty or all [`Effect::flags`] are default then it is
507    /// equivalent (and faster) to call [`Self::text`] instead.
508    ///
509    /// [`ConfigCx::text_configure`] should be called prior to this method to
510    /// select a font, font size and wrap options (based on the [`TextClass`]).
511    fn text_effects(
512        &mut self,
513        id: &Id,
514        pos: Coord,
515        rect: Rect,
516        text: &TextDisplay,
517        effects: &[Effect],
518    );
519
520    /// Method used to implement [`DrawCx::text_selected`]
521    fn text_selected_range(
522        &mut self,
523        id: &Id,
524        pos: Coord,
525        rect: Rect,
526        text: &TextDisplay,
527        range: Range<usize>,
528    );
529
530    /// Draw an edit marker at the given `byte` index on this `text`
531    ///
532    /// [`ConfigCx::text_configure`] should be called prior to this method to
533    /// select a font, font size and wrap options (based on the [`TextClass`]).
534    fn text_cursor(&mut self, id: &Id, pos: Coord, rect: Rect, text: &TextDisplay, byte: usize);
535
536    /// Draw UI element: check box
537    ///
538    /// The check box is a small visual element, typically a distinctive square
539    /// box with or without a "check" selection mark.
540    ///
541    /// The theme may animate transitions. To achieve this, `last_change` should be
542    /// the time of the last state change caused by the user, or none when the
543    /// last state change was programmatic.
544    fn check_box(&mut self, id: &Id, rect: Rect, checked: bool, last_change: Option<Instant>);
545
546    /// Draw UI element: radio button
547    ///
548    /// The radio box is a small visual element, typically a disinctive
549    /// circular box with or without a "radio" selection mark.
550    ///
551    /// The theme may animate transitions. To achieve this, `last_change` should be
552    /// the time of the last state change caused by the user, or none when the
553    /// last state change was programmatic.
554    fn radio_box(&mut self, id: &Id, rect: Rect, checked: bool, last_change: Option<Instant>);
555
556    /// Draw UI element: mark
557    fn mark(&mut self, id: &Id, rect: Rect, style: MarkStyle);
558
559    /// Draw UI element: scroll bar
560    ///
561    /// -   `id`: [`Id`] of the bar
562    /// -   `grip_id`: [`Id`] of the grip
563    /// -   `rect`: area of whole widget (slider track)
564    /// -   `grip_rect`: area of slider grip
565    /// -   `dir`: direction of bar
566    fn scroll_bar(&mut self, id: &Id, grip_id: &Id, rect: Rect, grip_rect: Rect, dir: Direction);
567
568    /// Draw UI element: slider
569    ///
570    /// -   `id`: [`Id`] of the bar
571    /// -   `grip_id`: [`Id`] of the grip
572    /// -   `rect`: area of whole widget (slider track)
573    /// -   `grip_rect`: area of slider grip
574    /// -   `dir`: direction of slider (currently only LTR or TTB)
575    fn slider(&mut self, id: &Id, grip_id: &Id, rect: Rect, grip_rect: Rect, dir: Direction);
576
577    /// Draw UI element: progress bar
578    ///
579    /// -   `id`: [`Id`] of the bar
580    /// -   `rect`: area of whole widget
581    /// -   `dir`: direction of progress bar
582    /// -   `value`: progress value, between 0.0 and 1.0
583    fn progress_bar(&mut self, id: &Id, rect: Rect, dir: Direction, value: f32);
584
585    /// Draw an image
586    fn image(&mut self, id: ImageId, rect: Rect);
587}
588
589#[cfg(test)]
590mod test {
591    use super::*;
592
593    fn _draw_ext(mut draw: DrawCx) {
594        // We can't call this method without constructing an actual ThemeDraw.
595        // But we don't need to: we just want to test that methods are callable.
596
597        let _scale = draw.size_cx().scale_factor();
598
599        let text = crate::theme::Text::new("sample", TextClass::Label(false));
600        draw.text_selected(Coord::ZERO, Rect::ZERO, &text, 0..6)
601    }
602}