1use std::time::Duration;
4
5use ratatui_core::buffer::{Buffer, Cell};
6use ratatui_core::style::Color;
7use unicode_segmentation::UnicodeSegmentation;
8
9use super::MeasureCx;
10use super::frame::{FocusRequest, Frame, Interaction, LayerEntry};
11use crate::color::Rgb;
12use crate::env::Env;
13use crate::geometry::{Rect, Size, clamp_u16};
14use crate::icons::PILLAR;
15use crate::keymap::KeyChord;
16use crate::motion::{Easing, Tweens};
17use crate::style::{CellStyle, WidgetStyle, to_color};
18use crate::text;
19use crate::theme::State;
20use crate::widget::memory::Memory;
21use crate::widget::{Key, LayoutProps, Node, WidgetId};
22
23pub struct PaintCx<'a> {
25 pub(crate) buf: &'a mut Buffer,
26 pub(crate) env: &'a Env,
27 pub(crate) frame: &'a mut Frame,
28 pub(crate) memory: &'a mut Memory,
29 pub(crate) interaction: &'a Interaction,
30 pub(crate) now: Duration,
31 pub(crate) clip: Rect,
32 pub(crate) id: WidgetId,
33 pub(crate) layout: LayoutProps,
34 pub(crate) scope: Option<WidgetId>,
35}
36
37impl PaintCx<'_> {
38 #[must_use]
40 pub fn id(&self) -> WidgetId {
41 self.id
42 }
43
44 #[must_use]
46 pub fn env(&self) -> &Env {
47 self.env
48 }
49
50 #[must_use]
52 pub fn layout(&self) -> LayoutProps {
53 self.layout
54 }
55
56 #[must_use]
58 pub fn now(&self) -> Duration {
59 self.now
60 }
61
62 #[must_use]
64 pub fn is_hovered(&self) -> bool {
65 self.interaction.hovered == Some(self.id)
66 }
67
68 #[must_use]
70 pub fn focused(&self) -> Option<WidgetId> {
71 self.interaction.focused
72 }
73
74 #[must_use]
76 pub fn pointer_anywhere(&self) -> Option<(i32, i32)> {
77 self.interaction.pointer
78 }
79
80 pub fn request_focus_within(&mut self, ancestor: WidgetId) {
83 self.frame.focus_request = Some(FocusRequest::Within(ancestor));
84 }
85
86 pub fn request_focus(&mut self, id: WidgetId) {
89 self.frame.focus_request = Some(FocusRequest::Exact(id));
90 }
91
92 pub fn register_dismissable(&mut self) {
100 self.frame.layers.push(LayerEntry { id: self.id, modal: false, surface: None });
101 }
102
103 #[must_use]
105 pub fn is_focused(&self) -> bool {
106 self.interaction.focused == Some(self.id)
107 }
108
109 #[must_use]
113 pub fn is_focus_visible(&self) -> bool {
114 self.is_focused() && !self.interaction.focus_by_pointer
115 }
116
117 #[must_use]
119 pub fn pointer(&self) -> Option<(i32, i32)> {
120 self.interaction.pointer.filter(|_| self.is_hovered())
121 }
122
123 pub fn is_pressed(&mut self) -> bool {
126 let Some((id, at)) = self.interaction.pressed else {
127 return false;
128 };
129 let end = at + self.env.theme().motion().flash;
130 if id != self.id || self.now >= end {
131 return false;
132 }
133 self.frame.schedule(end);
134 true
135 }
136
137 pub fn pressable_states(&mut self) -> Vec<State> {
141 let visible = self.is_focus_visible();
142 let mut states = self.states();
143 if !visible {
144 states.retain(|state| *state != State::Focus);
145 }
146 states
147 }
148
149 pub fn states(&mut self) -> Vec<State> {
151 let mut states = Vec::new();
152 if self.is_hovered() {
153 states.push(State::Hover);
154 }
155 if self.is_focused() {
156 states.push(State::Focus);
157 }
158 if self.is_pressed() {
159 states.push(State::Pressed);
160 }
161 states
162 }
163
164 pub fn style(&mut self, widget: &str, variant: Option<&str>, states: &[State]) -> WidgetStyle {
167 let props = self.env.theme().style(widget, variant, states);
168 let style = WidgetStyle::new(props, self.pulse_phase());
169 if style.is_animated() && !self.env.reduced_motion() {
170 self.request_frame_in(PULSE_FRAME);
171 }
172 style
173 }
174
175 #[must_use]
177 pub fn color(&self, token: &str) -> Rgb {
178 self.env.theme().color(token).unwrap_or(Rgb::new(0, 0, 0))
179 }
180
181 pub fn request_frame_in(&mut self, delay: Duration) {
183 self.frame.schedule(self.now + delay);
184 }
185
186 #[must_use]
188 pub fn reduced_motion(&self) -> bool {
189 self.env.reduced_motion()
190 }
191
192 pub fn animate(&mut self, name: &'static str, target: f32, duration: Duration, easing: Easing) -> f32 {
197 if self.env.reduced_motion() {
198 return target;
199 }
200 let now = self.now;
201 let persistent = self.scope.is_some();
202 let tween = self.memory.get::<Tweens>(self.id, persistent).drive(name, target, now, duration, easing);
203 if tween.is_running(now) {
204 self.request_frame_in(ANIMATION_FRAME);
205 }
206 tween.value(now)
207 }
208
209 pub fn progress_since(&mut self, start: Duration, duration: Duration, easing: Easing) -> f32 {
213 if self.env.reduced_motion() || duration.is_zero() {
214 return 1.0;
215 }
216 let elapsed = self.now.saturating_sub(start);
217 if elapsed >= duration {
218 return 1.0;
219 }
220 self.request_frame_in(ANIMATION_FRAME);
221 easing.apply(elapsed.as_secs_f32() / duration.as_secs_f32())
222 }
223
224 pub fn cycle(&mut self, period: Duration) -> f32 {
227 let period = period.as_millis().max(1);
228 if self.env.reduced_motion() {
229 return 0.0;
230 }
231 self.request_frame_in(ANIMATION_FRAME);
232 (self.now.as_millis() % period) as f32 / period as f32
233 }
234
235 pub fn ticks(&mut self, interval: Duration) -> u128 {
238 let interval_ms = interval.as_millis().max(1);
239 if self.env.reduced_motion() {
240 return 0;
241 }
242 let elapsed = self.now.as_millis();
243 let into = u64::try_from(elapsed % interval_ms).unwrap_or(0);
244 self.request_frame_in(Duration::from_millis(u64::try_from(interval_ms).unwrap_or(u64::MAX) - into));
245 elapsed / interval_ms
246 }
247
248 #[must_use]
250 pub fn pulse_phase(&self) -> f32 {
251 if self.env.reduced_motion() {
252 return 0.0;
253 }
254 let period = self.env.theme().motion().pulse_period.as_secs_f64();
255 let phase = self.now.as_secs_f64().rem_euclid(period) / period;
256 phase as f32
258 }
259
260 #[must_use]
262 pub fn clip(&self) -> Rect {
263 self.clip
264 }
265
266 pub(crate) fn can_paste(&self) -> bool {
269 self.interaction.can_paste
270 }
271
272 pub fn register_hit(&mut self, rect: Rect) {
274 self.register_hit_as(rect, self.id);
275 }
276
277 pub(crate) fn register_hit_as(&mut self, rect: Rect, id: WidgetId) {
280 if let Some(visible) = self.visible_part(rect) {
281 self.frame.hits.push((visible, id));
282 }
283 }
284
285 pub fn unselectable(&mut self, rect: Rect) {
288 if let Some(visible) = self.visible_part(rect) {
289 self.frame.unselectable.push(visible);
290 }
291 }
292
293 pub fn selectable(&mut self, rect: Rect) {
300 if let Some(visible) = self.visible_part(rect) {
301 self.frame.selectable.push((visible, self.id));
302 }
303 }
304
305 pub fn decoration(&mut self, rect: Rect) {
309 if let Some(visible) = self.visible_part(rect) {
310 self.frame.decorations.push(visible);
311 }
312 }
313
314 pub fn register_focusable(&mut self) {
316 self.frame.focusable.push(self.id);
317 }
318
319 pub fn request_overlay(&mut self, anchor: Rect) {
321 self.frame.overlays.push((self.id, anchor));
322 }
323
324 pub fn open_layer(&mut self) -> Duration {
350 self.frame.layers.push(LayerEntry { id: self.id, modal: true, surface: None });
351 self.request_focus_within(self.id);
352 self.interaction.layers.iter().find(|layer| layer.id == self.id).map_or(self.now, |layer| layer.opened)
353 }
354
355 pub(crate) fn set_layer_surface(&mut self, surface: Rect) {
358 let id = self.id;
359 if let Some(layer) = self.frame.layers.iter_mut().rev().find(|layer| layer.id == id) {
360 layer.surface = Some(surface);
361 }
362 }
363
364 pub(crate) fn modal_surfaces(&self, screen: Rect) -> Vec<Rect> {
367 self.frame.layers.iter().filter(|layer| layer.modal).map(|layer| layer.surface.unwrap_or(screen)).collect()
368 }
369
370 pub fn listen_key(&mut self, chord: KeyChord) {
376 self.frame.listeners.push((chord, self.id));
377 }
378
379 pub fn tint(&mut self, rect: Rect, color: Rgb, amount: f32) {
383 let amount = amount.clamp(0.0, 1.0);
384 if amount <= 0.0 {
385 return;
386 }
387 let depth = self.env.depth();
388 let blend = |current: Color| match current {
389 Color::Rgb(r, g, b) => Some(to_color(Rgb::new(r, g, b).mix(color, amount), depth)),
390 _ if amount > 0.5 => Some(to_color(color, depth)),
391 _ => None,
392 };
393 self.each_cell(rect, |cell| {
394 if let Some(fg) = blend(cell.fg) {
395 cell.fg = fg;
396 }
397 if let Some(bg) = blend(cell.bg) {
398 cell.bg = bg;
399 }
400 });
401 }
402
403 pub fn memory<T: Default + 'static>(&mut self) -> &mut T {
405 self.memory.get::<T>(self.id, self.scope.is_some())
406 }
407
408 pub fn fill(&mut self, rect: Rect, color: Rgb) {
410 let bg = to_color(color, self.env.depth());
411 self.each_cell(rect, |cell| cell.bg = bg);
412 }
413
414 pub(crate) fn fill_keeping_text_readable(&mut self, rect: Rect, color: Rgb, readable: Rgb) {
418 let depth = self.env.depth();
419 let (bg, text) = (to_color(color, depth), to_color(readable, depth));
420 self.each_cell(rect, |cell| {
421 cell.bg = bg;
422 if let Color::Rgb(r, g, b) = cell.fg
423 && cell.symbol().trim() != ""
424 && Rgb::new(r, g, b).contrast_ratio(color) < 3.0
425 {
426 cell.fg = text;
427 }
428 });
429 }
430
431 pub fn clear(&mut self, rect: Rect, color: Rgb) {
433 let mut blank = Cell::EMPTY;
436 blank.bg = to_color(color, self.env.depth());
437 self.each_cell(rect, |cell| cell.clone_from(&blank));
438 }
439
440 pub fn pillar(&mut self, x: i32, y: i32, color: Rgb) {
444 self.decoration(Rect::new(x, y, 1, 1));
445 let env = self.env;
446 let glyph = env.icons().glyph(PILLAR);
447 if glyph.trim().is_empty() {
448 self.fill(Rect::new(x, y, 1, 1), color);
449 } else {
450 self.text(x, y, &glyph, CellStyle::fg(color), 1);
451 }
452 }
453
454 pub fn text(&mut self, x: i32, y: i32, text: &str, style: CellStyle, max: u16) -> u16 {
457 let paint = style.for_depth(self.env.depth());
458 let clip = self.clip;
459 let limit = x + i32::from(max);
460 let mut column = x;
461 let drawn = text.len().min(usize::from(max));
464 if text.get(..text.len().min(drawn + 1)).is_some_and(text::is_printable_ascii) {
465 for index in 0..drawn {
466 if clip.contains(column, y)
467 && let Some(cell) = self.cell_mut(column, y)
468 {
469 cell.set_symbol(&text[index..=index]);
470 paint.apply(cell);
471 }
472 column += 1;
473 }
474 return clamp_u16(column - x);
475 }
476 for grapheme in text.graphemes(true) {
477 let width = i32::from(text::grapheme_width(grapheme));
478 if width == 0 {
479 continue;
480 }
481 if column + width > limit {
482 break;
483 }
484 for offset in 0..width {
485 let cx = column + offset;
486 if !clip.contains(cx, y) {
487 continue;
488 }
489 let whole = clip.contains(column, y) && clip.contains(column + width - 1, y);
490 if let Some(cell) = self.cell_mut(cx, y) {
491 let symbol = match (offset, whole) {
492 (0, true) => grapheme,
493 (_, true) => "",
494 _ => " ",
495 };
496 cell.set_symbol(symbol);
497 paint.apply(cell);
498 }
499 }
500 column += width;
501 }
502 clamp_u16(column - x)
503 }
504
505 pub fn paint_child<M: 'static>(&mut self, node: &Node<M>, rect: Rect) {
507 let saved = (self.id, self.layout, self.clip, self.scope);
508 self.id = node.id;
509 self.layout = node.layout;
510 if node.persistent {
511 self.scope = Some(node.id);
512 }
513 self.clip = saved.2.intersect(rect);
514 self.frame.rects.insert(node.id, rect);
515 self.frame.parents.insert(node.id, saved.0);
516 if let Key::Named(name) = &node.key {
517 self.frame.names.insert(node.id, name.clone());
518 }
519 if let Some(scope) = self.scope {
520 self.frame.scopes.insert(node.id, scope);
521 }
522 self.memory.touch(node.id, self.scope.is_some());
523 match node.selectable {
524 Some(true) => self.selectable(rect),
525 Some(false) => self.unselectable(rect),
526 None => {}
527 }
528 if node.widget.focusable() {
529 self.register_focusable();
530 }
531 node.widget.paint(self, rect.inset(node.layout.padding));
532 (self.id, self.layout, self.clip, self.scope) = saved;
533 }
534
535 pub fn paint_child_unfocusable<M: 'static>(&mut self, node: &Node<M>, rect: Rect) {
539 let registered = self.frame.focusable.len();
540 self.paint_child(node, rect);
541 self.frame.focusable.truncate(registered);
542 }
543
544 #[must_use]
547 pub fn pointer_within(&self) -> Option<(i32, i32)> {
548 self.interaction.pointer.filter(|_| self.interaction.hovered_chain.contains(&self.id))
549 }
550
551 #[must_use]
555 pub fn has_focus_within(&self) -> bool {
556 self.interaction.focused.is_some_and(|focused| self.frame.is_within(focused, self.id))
557 }
558
559 pub fn measure_child<M: 'static>(&mut self, node: &Node<M>, available: Size) -> Size {
561 MeasureCx::for_frame(self.env, &mut self.frame.measures).measure_child(node, available)
562 }
563
564 pub fn with_clip(&mut self, rect: Rect, paint: impl FnOnce(&mut Self)) {
566 let saved = self.clip;
567 self.clip = saved.intersect(rect);
568 paint(self);
569 self.clip = saved;
570 }
571
572 fn visible_part(&self, rect: Rect) -> Option<Rect> {
574 Some(rect.intersect(self.clip)).filter(|visible| !visible.is_empty())
575 }
576
577 pub(super) fn each_cell(&mut self, rect: Rect, mut change: impl FnMut(&mut Cell)) {
579 let area = rect.intersect(self.clip);
580 for y in area.y..area.bottom() {
581 for x in area.x..area.right() {
582 if let Some(cell) = self.cell_mut(x, y) {
583 change(cell);
584 }
585 }
586 }
587 }
588
589 fn cell_mut(&mut self, x: i32, y: i32) -> Option<&mut Cell> {
591 self.buf.cell_mut((u16::try_from(x).ok()?, u16::try_from(y).ok()?))
592 }
593}
594
595pub(crate) const ANIMATION_FRAME: Duration = Duration::from_millis(16);
597
598pub(crate) const PULSE_FRAME: Duration = Duration::from_millis(50);