1use 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#[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 #[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 #[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 #[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 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 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 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 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 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}