1use super::*;
2use crate::KeyMap;
3
4impl Context {
5 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
16 let content = s.into();
17 let default_fg = self
18 .rollback
19 .text_color_stack
20 .iter()
21 .rev()
22 .find_map(|c| *c)
23 .unwrap_or(self.theme.text);
24 self.commands.push(Command::Text {
25 content,
26 cursor_offset: None,
27 style: Style::new().fg(default_fg),
28 grow: 0,
29 align: Align::Start,
30 wrap: false,
31 truncate: false,
32 margin: Margin::default(),
33 constraints: Constraints::default(),
34 });
35 self.rollback.last_text_idx = Some(self.commands.len() - 1);
36 self
37 }
38
39 #[allow(clippy::print_stderr)]
45 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
46 let url_str = url.into();
47 let focused = self.register_focusable();
48 let (_interaction_id, response) = self.begin_widget_interaction(focused);
49
50 let activated = response.clicked || self.consume_activation_keys(focused);
51
52 if activated && let Err(e) = open_url(&url_str) {
53 eprintln!("[slt] failed to open URL: {e}");
54 }
55
56 let style = if focused {
57 Style::new()
58 .fg(self.theme.primary)
59 .bg(self.theme.surface_hover)
60 .underline()
61 .bold()
62 } else if response.hovered {
63 Style::new()
64 .fg(self.theme.accent)
65 .bg(self.theme.surface_hover)
66 .underline()
67 } else {
68 Style::new().fg(self.theme.primary).underline()
69 };
70
71 self.commands.push(Command::Link {
72 text: text.into(),
73 url: url_str,
74 style,
75 margin: Margin::default(),
76 constraints: Constraints::default(),
77 });
78 self.rollback.last_text_idx = Some(self.commands.len() - 1);
79 self
80 }
81
82 pub fn timer_display(&mut self, elapsed: std::time::Duration) -> &mut Self {
86 let total_centis = elapsed.as_millis() / 10;
87 let centis = total_centis % 100;
88 let total_seconds = total_centis / 100;
89 let seconds = total_seconds % 60;
90 let minutes = (total_seconds / 60) % 60;
91 let hours = total_seconds / 3600;
92
93 let content = if hours > 0 {
94 format!("{hours:02}:{minutes:02}:{seconds:02}.{centis:02}")
95 } else {
96 format!("{minutes:02}:{seconds:02}.{centis:02}")
97 };
98
99 self.commands.push(Command::Text {
100 content,
101 cursor_offset: None,
102 style: Style::new().fg(self.theme.text),
103 grow: 0,
104 align: Align::Start,
105 wrap: false,
106 truncate: false,
107 margin: Margin::default(),
108 constraints: Constraints::default(),
109 });
110 self.rollback.last_text_idx = Some(self.commands.len() - 1);
111 self
112 }
113
114 pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
116 let pairs: Vec<(&str, &str)> = keymap
117 .visible_bindings()
118 .map(|binding| (binding.display.as_str(), binding.description.as_str()))
119 .collect();
120 self.help(&pairs)
121 }
122
123 pub fn bold(&mut self) -> &mut Self {
127 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
128 self
129 }
130
131 pub fn dim(&mut self) -> &mut Self {
136 let text_dim = self.theme.text_dim;
137 self.modify_last_style(|s| {
138 s.modifiers |= Modifiers::DIM;
139 if s.fg.is_none() {
140 s.fg = Some(text_dim);
141 }
142 });
143 self
144 }
145
146 pub fn italic(&mut self) -> &mut Self {
148 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
149 self
150 }
151
152 pub fn underline(&mut self) -> &mut Self {
154 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
155 self
156 }
157
158 pub fn reversed(&mut self) -> &mut Self {
160 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
161 self
162 }
163
164 pub fn strikethrough(&mut self) -> &mut Self {
166 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
167 self
168 }
169
170 pub fn fg(&mut self, color: Color) -> &mut Self {
172 self.modify_last_style(|s| s.fg = Some(color));
173 self
174 }
175
176 pub fn bg(&mut self, color: Color) -> &mut Self {
178 self.modify_last_style(|s| s.bg = Some(color));
179 self
180 }
181
182 pub fn gradient(&mut self, from: Color, to: Color) -> &mut Self {
184 if let Some(idx) = self.rollback.last_text_idx {
185 let replacement = match &self.commands[idx] {
186 Command::Text {
187 content,
188 style,
189 wrap,
190 align,
191 margin,
192 constraints,
193 ..
194 } => {
195 let chars: Vec<char> = content.chars().collect();
196 let len = chars.len();
197 let denom = len.saturating_sub(1).max(1) as f64;
198 let segments = chars
199 .into_iter()
200 .enumerate()
201 .map(|(i, ch)| {
202 let mut seg_style = *style;
203 seg_style.fg = Some(from.blend_f64(to, i as f64 / denom));
204 (ch.to_string(), seg_style)
205 })
206 .collect();
207
208 Some(Command::RichText {
209 segments,
210 wrap: *wrap,
211 align: *align,
212 margin: *margin,
213 constraints: *constraints,
214 })
215 }
216 _ => None,
217 };
218
219 if let Some(command) = replacement {
220 self.commands[idx] = command;
221 }
222 }
223
224 self
225 }
226
227 pub fn gradient_stops_f64(&mut self, stops: &[(f64, Color)]) -> &mut Self {
252 if stops.is_empty() {
253 return self;
254 }
255 let sorted = Self::sorted_gradient_stops(stops);
256 self.apply_char_gradient(false, |t| Self::sample_gradient_stops(&sorted, t));
257 self
258 }
259
260 #[deprecated(
262 since = "0.22.2",
263 note = "use Context::gradient_stops_f64() to keep public float APIs on f64"
264 )]
265 pub fn gradient_stops(&mut self, stops: &[(f32, Color)]) -> &mut Self {
266 let stops: Vec<(f64, Color)> = stops
267 .iter()
268 .map(|(pos, color)| (f64::from(*pos), *color))
269 .collect();
270 self.gradient_stops_f64(&stops)
271 }
272
273 pub fn bg_gradient(&mut self, from: Color, to: Color) -> &mut Self {
290 self.apply_char_gradient(true, |t| from.blend_f64(to, t));
291 self
292 }
293
294 pub fn bg_gradient_stops_f64(&mut self, stops: &[(f64, Color)]) -> &mut Self {
314 if stops.is_empty() {
315 return self;
316 }
317 let sorted = Self::sorted_gradient_stops(stops);
318 self.apply_char_gradient(true, |t| Self::sample_gradient_stops(&sorted, t));
319 self
320 }
321
322 #[deprecated(
324 since = "0.22.2",
325 note = "use Context::bg_gradient_stops_f64() to keep public float APIs on f64"
326 )]
327 pub fn bg_gradient_stops(&mut self, stops: &[(f32, Color)]) -> &mut Self {
328 let stops: Vec<(f64, Color)> = stops
329 .iter()
330 .map(|(pos, color)| (f64::from(*pos), *color))
331 .collect();
332 self.bg_gradient_stops_f64(&stops)
333 }
334
335 fn sorted_gradient_stops(stops: &[(f64, Color)]) -> Vec<(f64, Color)> {
338 let mut sorted: Vec<(f64, Color)> = stops
339 .iter()
340 .map(|(pos, color)| (pos.clamp(0.0, 1.0), *color))
341 .collect();
342 sorted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
343 sorted
344 }
345
346 fn sample_gradient_stops(stops: &[(f64, Color)], t: f64) -> Color {
349 let t = t.clamp(0.0, 1.0);
350 let first = match stops.first() {
352 Some(stop) => *stop,
353 None => return Color::Rgb(0, 0, 0),
354 };
355 let last = *stops.last().unwrap_or(&first);
356 if t <= first.0 {
357 return first.1;
358 }
359 if t >= last.0 {
360 return last.1;
361 }
362 for window in stops.windows(2) {
363 let (p0, c0) = window[0];
364 let (p1, c1) = window[1];
365 if t >= p0 && t <= p1 {
366 let span = p1 - p0;
367 if span <= f64::EPSILON {
368 return c1;
369 }
370 let local = (t - p0) / span;
371 return c1.blend_f64(c0, local);
372 }
373 }
374 last.1
375 }
376
377 fn apply_char_gradient(&mut self, is_bg: bool, color_at: impl Fn(f64) -> Color) {
381 if let Some(idx) = self.rollback.last_text_idx {
382 let replacement = match &self.commands[idx] {
383 Command::Text {
384 content,
385 style,
386 wrap,
387 align,
388 margin,
389 constraints,
390 ..
391 } => {
392 let chars: Vec<char> = content.chars().collect();
393 let len = chars.len();
394 let denom = len.saturating_sub(1).max(1) as f64;
395 let segments = chars
396 .into_iter()
397 .enumerate()
398 .map(|(i, ch)| {
399 let mut seg_style = *style;
400 let color = color_at(i as f64 / denom);
401 if is_bg {
402 seg_style.bg = Some(color);
403 } else {
404 seg_style.fg = Some(color);
405 }
406 (ch.to_string(), seg_style)
407 })
408 .collect();
409
410 Some(Command::RichText {
411 segments,
412 wrap: *wrap,
413 align: *align,
414 margin: *margin,
415 constraints: *constraints,
416 })
417 }
418 _ => None,
419 };
420
421 if let Some(command) = replacement {
422 self.commands[idx] = command;
423 }
424 }
425 }
426
427 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
429 let apply_group_style = self
430 .rollback
431 .group_stack
432 .last()
433 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
434 .unwrap_or(false);
435 if apply_group_style {
436 self.modify_last_style(|s| s.fg = Some(color));
437 }
438 self
439 }
440
441 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
443 let apply_group_style = self
444 .rollback
445 .group_stack
446 .last()
447 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
448 .unwrap_or(false);
449 if apply_group_style {
450 self.modify_last_style(|s| s.bg = Some(color));
451 }
452 self
453 }
454
455 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
460 self.styled_with_cursor(s, style, None)
461 }
462
463 pub(crate) fn styled_with_cursor(
464 &mut self,
465 s: impl Into<String>,
466 style: Style,
467 cursor_offset: Option<usize>,
468 ) -> &mut Self {
469 self.commands.push(Command::Text {
470 content: s.into(),
471 cursor_offset,
472 style,
473 grow: 0,
474 align: Align::Start,
475 wrap: false,
476 truncate: false,
477 margin: Margin::default(),
478 constraints: Constraints::default(),
479 });
480 self.rollback.last_text_idx = Some(self.commands.len() - 1);
481 self
482 }
483
484 pub fn wrap(&mut self) -> &mut Self {
486 if let Some(idx) = self.rollback.last_text_idx
487 && let Command::Text { wrap, .. } = &mut self.commands[idx]
488 {
489 *wrap = true;
490 }
491 self
492 }
493
494 pub fn truncate(&mut self) -> &mut Self {
497 if let Some(idx) = self.rollback.last_text_idx
498 && let Command::Text { truncate, .. } = &mut self.commands[idx]
499 {
500 *truncate = true;
501 }
502 self
503 }
504
505 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
506 if let Some(idx) = self.rollback.last_text_idx {
507 match &mut self.commands[idx] {
508 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
509 _ => {}
510 }
511 }
512 }
513
514 fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
515 if let Some(idx) = self.rollback.last_text_idx {
516 match &mut self.commands[idx] {
517 Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
518 f(constraints)
519 }
520 _ => {}
521 }
522 }
523 }
524
525 fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
526 if let Some(idx) = self.rollback.last_text_idx {
527 match &mut self.commands[idx] {
528 Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
529 _ => {}
530 }
531 }
532 }
533
534 pub fn grow(&mut self, value: u16) -> &mut Self {
541 if let Some(idx) = self.rollback.last_text_idx
542 && let Command::Text { grow, .. } = &mut self.commands[idx]
543 {
544 *grow = value;
545 }
546 self
547 }
548
549 pub fn align(&mut self, align: Align) -> &mut Self {
551 if let Some(idx) = self.rollback.last_text_idx
552 && let Command::Text {
553 align: text_align, ..
554 } = &mut self.commands[idx]
555 {
556 *text_align = align;
557 }
558 self
559 }
560
561 pub fn text_center(&mut self) -> &mut Self {
565 self.align(Align::Center)
566 }
567
568 pub fn text_right(&mut self) -> &mut Self {
571 self.align(Align::End)
572 }
573
574 pub fn w(&mut self, value: u32) -> &mut Self {
582 self.modify_last_constraints(|c| {
583 *c = c.w(value);
584 });
585 self
586 }
587
588 pub fn h(&mut self, value: u32) -> &mut Self {
592 self.modify_last_constraints(|c| {
593 *c = c.h(value);
594 });
595 self
596 }
597
598 pub fn min_w(&mut self, value: u32) -> &mut Self {
600 self.modify_last_constraints(|c| c.set_min_width(Some(value)));
601 self
602 }
603
604 pub fn max_w(&mut self, value: u32) -> &mut Self {
606 self.modify_last_constraints(|c| c.set_max_width(Some(value)));
607 self
608 }
609
610 pub fn min_h(&mut self, value: u32) -> &mut Self {
612 self.modify_last_constraints(|c| c.set_min_height(Some(value)));
613 self
614 }
615
616 pub fn max_h(&mut self, value: u32) -> &mut Self {
618 self.modify_last_constraints(|c| c.set_max_height(Some(value)));
619 self
620 }
621
622 pub fn m(&mut self, value: u32) -> &mut Self {
626 self.modify_last_margin(|m| *m = Margin::all(value));
627 self
628 }
629
630 pub fn mx(&mut self, value: u32) -> &mut Self {
632 self.modify_last_margin(|m| {
633 m.left = value;
634 m.right = value;
635 });
636 self
637 }
638
639 pub fn my(&mut self, value: u32) -> &mut Self {
641 self.modify_last_margin(|m| {
642 m.top = value;
643 m.bottom = value;
644 });
645 self
646 }
647
648 pub fn mt(&mut self, value: u32) -> &mut Self {
650 self.modify_last_margin(|m| m.top = value);
651 self
652 }
653
654 pub fn mr(&mut self, value: u32) -> &mut Self {
656 self.modify_last_margin(|m| m.right = value);
657 self
658 }
659
660 pub fn mb(&mut self, value: u32) -> &mut Self {
662 self.modify_last_margin(|m| m.bottom = value);
663 self
664 }
665
666 pub fn ml(&mut self, value: u32) -> &mut Self {
668 self.modify_last_margin(|m| m.left = value);
669 self
670 }
671
672 pub fn spacer(&mut self) -> &mut Self {
676 self.commands.push(Command::Spacer { grow: 1 });
677 self.rollback.last_text_idx = None;
678 self
679 }
680
681 pub fn with_if(&mut self, cond: bool, f: impl FnOnce(&mut Self)) -> &mut Self {
710 if cond {
711 f(self);
712 }
713 self
714 }
715
716 pub fn with(&mut self, f: impl FnOnce(&mut Self)) -> &mut Self {
730 f(self);
731 self
732 }
733}
734
735#[cfg(test)]
736mod gradient_tests {
737 use super::*;
738 use crate::TestBackend;
739
740 #[test]
741 fn gradient_stops_interpolates_fg_across_columns() {
742 let red = Color::Rgb(255, 0, 0);
743 let blue = Color::Rgb(0, 0, 255);
744 let mut backend = TestBackend::new(20, 4);
745 backend.render(|ui| {
746 ui.text("ABC")
747 .gradient_stops_f64(&[(0.0, red), (1.0, blue)]);
748 });
749
750 let buf = backend.buffer();
751 assert_eq!(
754 buf.get(0, 0).style.fg,
755 Some(red),
756 "first column should be red"
757 );
758 assert_eq!(
759 buf.get(1, 0).style.fg,
760 Some(Color::Rgb(128, 0, 128)),
761 "middle column should be the halfway blend"
762 );
763 assert_eq!(
764 buf.get(2, 0).style.fg,
765 Some(blue),
766 "last column should be blue"
767 );
768 }
769
770 #[test]
771 fn gradient_stops_unsorted_input_is_sorted() {
772 let red = Color::Rgb(255, 0, 0);
773 let blue = Color::Rgb(0, 0, 255);
774 let mut backend = TestBackend::new(20, 4);
775 backend.render(|ui| {
776 ui.text("ABC")
778 .gradient_stops_f64(&[(1.0, blue), (0.0, red)]);
779 });
780
781 let buf = backend.buffer();
782 assert_eq!(buf.get(0, 0).style.fg, Some(red));
783 assert_eq!(buf.get(2, 0).style.fg, Some(blue));
784 }
785
786 #[test]
787 fn gradient_stops_multi_stop_hits_middle_stop_exactly() {
788 let red = Color::Rgb(255, 0, 0);
789 let green = Color::Rgb(0, 255, 0);
790 let blue = Color::Rgb(0, 0, 255);
791 let mut backend = TestBackend::new(20, 4);
792 backend.render(|ui| {
793 ui.text("ABC")
795 .gradient_stops_f64(&[(0.0, red), (0.5, green), (1.0, blue)]);
796 });
797
798 let buf = backend.buffer();
799 assert_eq!(buf.get(0, 0).style.fg, Some(red), "t=0 → first stop");
800 assert_eq!(
801 buf.get(1, 0).style.fg,
802 Some(green),
803 "t=0.5 → middle stop exactly"
804 );
805 assert_eq!(buf.get(2, 0).style.fg, Some(blue), "t=1 → last stop");
806 }
807
808 #[test]
809 fn gradient_stops_single_stop_is_solid() {
810 let cyan = Color::Rgb(0, 200, 200);
811 let mut backend = TestBackend::new(20, 4);
812 backend.render(|ui| {
813 ui.text("ABCD").gradient_stops_f64(&[(0.0, cyan)]);
814 });
815
816 let buf = backend.buffer();
817 for x in 0..4 {
818 assert_eq!(
819 buf.get(x, 0).style.fg,
820 Some(cyan),
821 "every column should be the single solid stop"
822 );
823 }
824 }
825
826 #[test]
827 fn gradient_stops_empty_is_noop() {
828 let mut backend = TestBackend::new(20, 4);
829 backend.render(|ui| {
830 ui.text("HELLO").gradient_stops_f64(&[]);
832 });
833
834 backend.assert_contains("HELLO");
835 }
836
837 #[test]
838 fn bg_gradient_applies_to_background() {
839 let red = Color::Rgb(255, 0, 0);
840 let blue = Color::Rgb(0, 0, 255);
841 let mut backend = TestBackend::new(20, 4);
842 backend.render(|ui| {
843 ui.text("ABC").bg_gradient(red, blue);
844 });
845
846 let buf = backend.buffer();
847 assert_eq!(buf.get(0, 0).style.bg, Some(blue), "first column bg = to");
850 assert_eq!(buf.get(2, 0).style.bg, Some(red), "last column bg = from");
851 assert_eq!(
852 buf.get(1, 0).style.bg,
853 Some(Color::Rgb(128, 0, 128)),
854 "middle column bg = halfway blend"
855 );
856 }
857
858 #[test]
859 fn bg_gradient_stops_interpolates_background() {
860 let red = Color::Rgb(255, 0, 0);
861 let blue = Color::Rgb(0, 0, 255);
862 let mut backend = TestBackend::new(20, 4);
863 backend.render(|ui| {
864 ui.text("ABC")
865 .bg_gradient_stops_f64(&[(0.0, red), (1.0, blue)]);
866 });
867
868 let buf = backend.buffer();
869 assert_eq!(buf.get(0, 0).style.bg, Some(red), "first column bg = red");
870 assert_eq!(
871 buf.get(1, 0).style.bg,
872 Some(Color::Rgb(128, 0, 128)),
873 "middle column bg = halfway blend"
874 );
875 assert_eq!(buf.get(2, 0).style.bg, Some(blue), "last column bg = blue");
876 }
877
878 #[test]
879 fn bg_gradient_stops_empty_is_noop() {
880 let mut backend = TestBackend::new(20, 4);
881 backend.render(|ui| {
882 ui.text("WORLD").bg_gradient_stops_f64(&[]);
883 });
884
885 backend.assert_contains("WORLD");
886 }
887}