oxi_tui/widget/
context.rs1use std::time::Instant;
15
16use ratatui::Frame;
17use ratatui::buffer::Buffer;
18use ratatui::layout::{Position, Rect};
19
20use crate::pipeline::CursorSlot;
21use crate::pipeline::diff_backend::{LinkCollector, LinkTarget};
22use crate::theme::{TerminalCaps, Theme};
23use crate::widget::CellRange;
24
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
27pub enum FocusTarget {
28 #[default]
29 None,
30 Chat,
31 Input,
32 Overlay,
33}
34
35pub struct RenderCtx<'a, 'f> {
42 frame: &'a mut Frame<'f>,
43 theme: &'a Theme,
44 caps: &'a TerminalCaps,
45 pub focus: FocusTarget,
46 pub time: Instant,
47 links: LinkCollector,
48 cursor: CursorSlot,
49}
50
51impl<'a, 'f> RenderCtx<'a, 'f> {
52 pub fn new(frame: &'a mut Frame<'f>, theme: &'a Theme, caps: &'a TerminalCaps) -> Self {
53 Self {
54 frame,
55 theme,
56 caps,
57 focus: FocusTarget::default(),
58 time: Instant::now(),
59 links: LinkCollector::new(),
60 cursor: CursorSlot::NotSet,
61 }
62 }
63
64 #[must_use]
65 pub fn theme(&self) -> &Theme {
66 self.theme
67 }
68
69 #[must_use]
70 pub fn caps(&self) -> &TerminalCaps {
71 self.caps
72 }
73
74 pub fn buffer_mut(&mut self) -> &mut Buffer {
75 self.frame.buffer_mut()
76 }
77
78 #[must_use]
79 pub fn area(&self) -> Rect {
80 self.frame.area()
81 }
82
83 pub fn set_cursor(&mut self, pos: Position) {
84 self.cursor = CursorSlot::Show(pos);
85 }
86
87 pub fn hide_cursor(&mut self) {
88 self.cursor = CursorSlot::Hide;
89 }
90
91 pub(crate) fn take_cursor_slot(&mut self) -> CursorSlot {
94 std::mem::replace(&mut self.cursor, CursorSlot::NotSet)
95 }
96
97 pub fn emit_link(&mut self, range: CellRange, target: LinkTarget) {
98 self.links.add(range, target);
99 }
100
101 #[expect(dead_code, reason = "used by pipeline link flush in Task 9")]
103 pub(crate) fn take_links(&mut self) -> LinkCollector {
104 std::mem::take(&mut self.links)
105 }
106 pub fn with_frame<R>(&mut self, f: impl FnOnce(&mut Frame<'f>) -> R) -> R {
113 f(self.frame)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use ratatui::Terminal;
121 use ratatui::backend::TestBackend;
122
123 fn make_ctx<'a, 'f>(
124 frame: &'a mut Frame<'f>,
125 theme: &'a Theme,
126 caps: &'a TerminalCaps,
127 ) -> RenderCtx<'a, 'f> {
128 RenderCtx::new(frame, theme, caps)
129 }
130
131 #[test]
132 fn cursor_starts_notset() {
133 let backend = TestBackend::new(10, 5);
134 let mut term = Terminal::new(backend).unwrap();
135 let theme = Theme::dark();
136 let caps = TerminalCaps::default();
137 term.draw(|f| {
138 let mut ctx = make_ctx(f, &theme, &caps);
139 let slot = ctx.take_cursor_slot();
140 assert_eq!(slot, CursorSlot::NotSet);
141 })
142 .unwrap();
143 }
144
145 #[test]
146 fn set_cursor_makes_slot_show() {
147 let backend = TestBackend::new(10, 5);
148 let mut term = Terminal::new(backend).unwrap();
149 let theme = Theme::dark();
150 let caps = TerminalCaps::default();
151 term.draw(|f| {
152 let mut ctx = make_ctx(f, &theme, &caps);
153 ctx.set_cursor(Position { x: 3, y: 4 });
154 let slot = ctx.take_cursor_slot();
155 assert_eq!(slot, CursorSlot::Show(Position { x: 3, y: 4 }));
156 })
157 .unwrap();
158 }
159
160 #[test]
161 fn hide_cursor_makes_slot_hide() {
162 let backend = TestBackend::new(10, 5);
163 let mut term = Terminal::new(backend).unwrap();
164 let theme = Theme::dark();
165 let caps = TerminalCaps::default();
166 term.draw(|f| {
167 let mut ctx = make_ctx(f, &theme, &caps);
168 ctx.hide_cursor();
169 let slot = ctx.take_cursor_slot();
170 assert_eq!(slot, CursorSlot::Hide);
171 })
172 .unwrap();
173 }
174 #[test]
175 fn with_frame_provides_access_to_frame() {
176 let backend = TestBackend::new(10, 5);
177 let mut term = Terminal::new(backend).unwrap();
178 let theme = Theme::dark();
179 let caps = TerminalCaps::default();
180 term.draw(|f| {
181 let mut ctx = make_ctx(f, &theme, &caps);
182 ctx.with_frame(|frame| {
184 let area = frame.area();
185 assert_eq!(area.width, 10);
186 assert_eq!(area.height, 5);
187 });
188 ctx.with_frame(|frame| {
191 let cell = &mut frame.buffer_mut()[(0, 0)];
192 cell.set_symbol("Z");
193 });
194 assert_eq!(ctx.area().width, 10);
196 })
197 .unwrap();
198 }
199
200 #[test]
201 fn take_resets_to_notset() {
202 let backend = TestBackend::new(10, 5);
203 let mut term = Terminal::new(backend).unwrap();
204 let theme = Theme::dark();
205 let caps = TerminalCaps::default();
206 term.draw(|f| {
207 let mut ctx = make_ctx(f, &theme, &caps);
208 ctx.set_cursor(Position { x: 0, y: 0 });
209 let _ = ctx.take_cursor_slot();
210 let slot2 = ctx.take_cursor_slot();
211 assert_eq!(slot2, CursorSlot::NotSet);
212 })
213 .unwrap();
214 }
215}