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}