1use super::*;
2
3impl Context {
4 pub(crate) fn new(
5 events: Vec<Event>,
6 width: u32,
7 height: u32,
8 state: &mut FrameState,
9 theme: Theme,
10 ) -> Self {
11 let hook_states = &mut state.hook_states;
12 let focus = &mut state.focus;
13 let layout_feedback = &mut state.layout_feedback;
14 let diagnostics = &mut state.diagnostics;
15 let consumed = vec![false; events.len()];
16
17 let mut mouse_pos = layout_feedback.last_mouse_pos;
18 let mut click_pos = None;
19 for event in &events {
20 if let Event::Mouse(mouse) = event {
21 mouse_pos = Some((mouse.x, mouse.y));
22 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
23 click_pos = Some((mouse.x, mouse.y));
24 }
25 }
26 }
27
28 let mut focus_index = focus.focus_index;
29 if let Some((mx, my)) = click_pos {
30 let mut best: Option<(usize, u64)> = None;
31 for &(fid, rect) in &layout_feedback.prev_focus_rects {
32 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
33 let area = rect.width as u64 * rect.height as u64;
34 if best.map_or(true, |(_, ba)| area < ba) {
35 best = Some((fid, area));
36 }
37 }
38 }
39 if let Some((fid, _)) = best {
40 focus_index = fid;
41 }
42 }
43
44 Self {
45 commands: Vec::new(),
46 events,
47 consumed,
48 should_quit: false,
49 area_width: width,
50 area_height: height,
51 tick: diagnostics.tick,
52 focus_index,
53 hook_states: std::mem::take(hook_states),
54 prev_focus_count: focus.prev_focus_count,
55 prev_modal_focus_start: focus.prev_modal_focus_start,
56 prev_modal_focus_count: focus.prev_modal_focus_count,
57 prev_scroll_infos: std::mem::take(&mut layout_feedback.prev_scroll_infos),
58 prev_scroll_rects: std::mem::take(&mut layout_feedback.prev_scroll_rects),
59 prev_hit_map: std::mem::take(&mut layout_feedback.prev_hit_map),
60 prev_group_rects: std::mem::take(&mut layout_feedback.prev_group_rects),
61 prev_focus_groups: std::mem::take(&mut layout_feedback.prev_focus_groups),
62 _prev_focus_rects: std::mem::take(&mut layout_feedback.prev_focus_rects),
63 mouse_pos,
64 click_pos,
65 prev_modal_active: focus.prev_modal_active,
66 clipboard_text: None,
67 debug: diagnostics.debug_mode,
68 theme,
69 is_real_terminal: false,
70 deferred_draws: Vec::new(),
71 rollback: ContextRollbackState {
72 last_text_idx: None,
73 focus_count: 0,
74 interaction_count: 0,
75 scroll_count: 0,
76 group_count: 0,
77 group_stack: Vec::new(),
78 overlay_depth: 0,
79 modal_active: false,
80 modal_focus_start: 0,
81 modal_focus_count: 0,
82 hook_cursor: 0,
83 dark_mode: theme.is_dark,
84 notification_queue: std::mem::take(&mut diagnostics.notification_queue),
85 pending_tooltips: Vec::new(),
86 text_color_stack: Vec::new(),
87 },
88 scroll_lines_per_event: 1,
89 }
90 }
91
92 pub fn set_scroll_speed(&mut self, lines: u32) {
94 self.scroll_lines_per_event = lines.max(1);
95 }
96
97 pub fn scroll_speed(&self) -> u32 {
99 self.scroll_lines_per_event
100 }
101
102 pub fn focus_index(&self) -> usize {
107 self.focus_index
108 }
109
110 pub fn set_focus_index(&mut self, index: usize) {
125 self.focus_index = index;
126 }
127
128 #[allow(clippy::misnamed_getters)]
137 pub fn focus_count(&self) -> usize {
138 self.prev_focus_count
139 }
140
141 pub(crate) fn process_focus_keys(&mut self) {
142 for (i, event) in self.events.iter().enumerate() {
143 if self.consumed[i] {
144 continue;
145 }
146 if let Event::Key(key) = event {
147 if key.kind != KeyEventKind::Press {
148 continue;
149 }
150 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
151 if self.prev_modal_active && self.prev_modal_focus_count > 0 {
152 let mut modal_local =
153 self.focus_index.saturating_sub(self.prev_modal_focus_start);
154 modal_local %= self.prev_modal_focus_count;
155 let next = (modal_local + 1) % self.prev_modal_focus_count;
156 self.focus_index = self.prev_modal_focus_start + next;
157 } else if self.prev_focus_count > 0 {
158 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
159 }
160 self.consumed[i] = true;
161 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
162 || key.code == KeyCode::BackTab
163 {
164 if self.prev_modal_active && self.prev_modal_focus_count > 0 {
165 let mut modal_local =
166 self.focus_index.saturating_sub(self.prev_modal_focus_start);
167 modal_local %= self.prev_modal_focus_count;
168 let prev = if modal_local == 0 {
169 self.prev_modal_focus_count - 1
170 } else {
171 modal_local - 1
172 };
173 self.focus_index = self.prev_modal_focus_start + prev;
174 } else if self.prev_focus_count > 0 {
175 self.focus_index = if self.focus_index == 0 {
176 self.prev_focus_count - 1
177 } else {
178 self.focus_index - 1
179 };
180 }
181 self.consumed[i] = true;
182 }
183 }
184 }
185 }
186
187 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
191 w.ui(self)
192 }
193
194 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
209 self.error_boundary_with(f, |ui, msg| {
210 ui.styled(
211 format!("⚠ Error: {msg}"),
212 Style::new().fg(ui.theme.error).bold(),
213 );
214 });
215 }
216
217 pub fn error_boundary_with(
237 &mut self,
238 f: impl FnOnce(&mut Context),
239 fallback: impl FnOnce(&mut Context, String),
240 ) {
241 let snapshot = ContextCheckpoint::capture(self);
242
243 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
244 f(self);
245 }));
246
247 match result {
248 Ok(()) => {}
249 Err(panic_info) => {
250 if self.is_real_terminal {
251 #[cfg(feature = "crossterm")]
252 {
253 let _ = crossterm::terminal::enable_raw_mode();
254 let _ = crossterm::execute!(
255 std::io::stdout(),
256 crossterm::terminal::EnterAlternateScreen
257 );
258 }
259
260 #[cfg(not(feature = "crossterm"))]
261 {}
262 }
263
264 snapshot.restore(self);
265
266 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
267 (*s).to_string()
268 } else if let Some(s) = panic_info.downcast_ref::<String>() {
269 s.clone()
270 } else {
271 "widget panicked".to_string()
272 };
273
274 fallback(self, msg);
275 }
276 }
277 }
278
279 pub(crate) fn reserve_interaction_slot(&mut self) -> usize {
281 let id = self.rollback.interaction_count;
282 self.rollback.interaction_count += 1;
283 id
284 }
285
286 pub(crate) fn skip_interaction_slot(&mut self) {
289 self.reserve_interaction_slot();
290 }
291
292 pub(crate) fn next_interaction_id(&mut self) -> usize {
294 let id = self.reserve_interaction_slot();
295 self.commands.push(Command::InteractionMarker(id));
296 id
297 }
298
299 pub fn interaction(&mut self) -> Response {
307 if (self.rollback.modal_active || self.prev_modal_active)
308 && self.rollback.overlay_depth == 0
309 {
310 return Response::none();
311 }
312 let id = self.next_interaction_id();
313 self.response_for(id)
314 }
315
316 pub(crate) fn begin_widget_interaction(&mut self, focused: bool) -> (usize, Response) {
317 let interaction_id = self.next_interaction_id();
318 let mut response = self.response_for(interaction_id);
319 response.focused = focused;
320 (interaction_id, response)
321 }
322
323 pub(crate) fn consume_indices<I>(&mut self, indices: I)
324 where
325 I: IntoIterator<Item = usize>,
326 {
327 for index in indices {
328 self.consumed[index] = true;
329 }
330 }
331
332 pub(crate) fn available_key_presses(
333 &self,
334 ) -> impl Iterator<Item = (usize, &crate::event::KeyEvent)> + '_ {
335 self.events.iter().enumerate().filter_map(|(i, event)| {
336 if self.consumed[i] {
337 return None;
338 }
339 match event {
340 Event::Key(key) if key.kind == KeyEventKind::Press => Some((i, key)),
341 _ => None,
342 }
343 })
344 }
345
346 pub(crate) fn available_pastes(&self) -> impl Iterator<Item = (usize, &str)> + '_ {
347 self.events.iter().enumerate().filter_map(|(i, event)| {
348 if self.consumed[i] {
349 return None;
350 }
351 match event {
352 Event::Paste(text) => Some((i, text.as_str())),
353 _ => None,
354 }
355 })
356 }
357
358 pub(crate) fn left_clicks_in_rect(
359 &self,
360 rect: Rect,
361 ) -> impl Iterator<Item = (usize, &crate::event::MouseEvent)> + '_ {
362 self.mouse_events_in_rect(rect).filter_map(|(i, mouse)| {
363 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
364 Some((i, mouse))
365 } else {
366 None
367 }
368 })
369 }
370
371 pub(crate) fn mouse_events_in_rect(
372 &self,
373 rect: Rect,
374 ) -> impl Iterator<Item = (usize, &crate::event::MouseEvent)> + '_ {
375 self.events
376 .iter()
377 .enumerate()
378 .filter_map(move |(i, event)| {
379 if self.consumed[i] {
380 return None;
381 }
382
383 let Event::Mouse(mouse) = event else {
384 return None;
385 };
386
387 if mouse.x < rect.x
388 || mouse.x >= rect.right()
389 || mouse.y < rect.y
390 || mouse.y >= rect.bottom()
391 {
392 return None;
393 }
394
395 Some((i, mouse))
396 })
397 }
398
399 pub(crate) fn left_clicks_for_interaction(
400 &self,
401 interaction_id: usize,
402 ) -> Option<(Rect, Vec<(usize, &crate::event::MouseEvent)>)> {
403 let rect = self.prev_hit_map.get(interaction_id).copied()?;
404 let clicks = self.left_clicks_in_rect(rect).collect();
405 Some((rect, clicks))
406 }
407
408 pub(crate) fn consume_activation_keys(&mut self, focused: bool) -> bool {
409 if !focused {
410 return false;
411 }
412
413 let consumed: Vec<usize> = self
414 .available_key_presses()
415 .filter_map(|(i, key)| {
416 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
417 Some(i)
418 } else {
419 None
420 }
421 })
422 .collect();
423 let activated = !consumed.is_empty();
424 self.consume_indices(consumed);
425 activated
426 }
427
428 pub fn register_focusable(&mut self) -> bool {
433 if (self.rollback.modal_active || self.prev_modal_active)
434 && self.rollback.overlay_depth == 0
435 {
436 return false;
437 }
438 let id = self.rollback.focus_count;
439 self.rollback.focus_count += 1;
440 self.commands.push(Command::FocusMarker(id));
441 if self.prev_modal_active
442 && self.prev_modal_focus_count > 0
443 && self.rollback.modal_active
444 && self.rollback.overlay_depth > 0
445 {
446 let mut modal_local_id = id.saturating_sub(self.rollback.modal_focus_start);
447 modal_local_id %= self.prev_modal_focus_count;
448 let mut modal_focus_idx = self.focus_index.saturating_sub(self.prev_modal_focus_start);
449 modal_focus_idx %= self.prev_modal_focus_count;
450 return modal_local_id == modal_focus_idx;
451 }
452 if self.prev_focus_count == 0 {
453 return true;
454 }
455 self.focus_index % self.prev_focus_count == id
456 }
457
458 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
476 let idx = self.rollback.hook_cursor;
477 self.rollback.hook_cursor += 1;
478
479 if idx >= self.hook_states.len() {
480 self.hook_states.push(Box::new(init()));
481 }
482
483 State::from_idx(idx)
484 }
485
486 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
494 &mut self,
495 deps: &D,
496 compute: impl FnOnce(&D) -> T,
497 ) -> &T {
498 let idx = self.rollback.hook_cursor;
499 self.rollback.hook_cursor += 1;
500
501 let should_recompute = if idx >= self.hook_states.len() {
502 true
503 } else {
504 let (stored_deps, _) = self.hook_states[idx]
505 .downcast_ref::<(D, T)>()
506 .unwrap_or_else(|| {
507 panic!(
508 "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
509 idx,
510 std::any::type_name::<(D, T)>()
511 )
512 });
513 stored_deps != deps
514 };
515
516 if should_recompute {
517 let value = compute(deps);
518 let slot = Box::new((deps.clone(), value));
519 if idx < self.hook_states.len() {
520 self.hook_states[idx] = slot;
521 } else {
522 self.hook_states.push(slot);
523 }
524 }
525
526 let (_, value) = self.hook_states[idx]
527 .downcast_ref::<(D, T)>()
528 .unwrap_or_else(|| {
529 panic!(
530 "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
531 idx,
532 std::any::type_name::<(D, T)>()
533 )
534 });
535 value
536 }
537
538 pub fn light_dark(&self, light: Color, dark: Color) -> Color {
540 if self.theme.is_dark {
541 dark
542 } else {
543 light
544 }
545 }
546
547 pub fn notify(&mut self, message: &str, level: ToastLevel) {
557 let tick = self.tick;
558 self.rollback
559 .notification_queue
560 .push((message.to_string(), level, tick));
561 }
562
563 pub(crate) fn render_notifications(&mut self) {
564 self.rollback
565 .notification_queue
566 .retain(|(_, _, created)| self.tick.saturating_sub(*created) < 180);
567 if self.rollback.notification_queue.is_empty() {
568 return;
569 }
570
571 let items: Vec<(String, Color)> = self
572 .rollback
573 .notification_queue
574 .iter()
575 .rev()
576 .map(|(message, level, _)| {
577 let color = match level {
578 ToastLevel::Info => self.theme.primary,
579 ToastLevel::Success => self.theme.success,
580 ToastLevel::Warning => self.theme.warning,
581 ToastLevel::Error => self.theme.error,
582 };
583 (message.clone(), color)
584 })
585 .collect();
586
587 let _ = self.overlay(|ui| {
588 let _ = ui.row(|ui| {
589 ui.spacer();
590 let _ = ui.col(|ui| {
591 for (message, color) in &items {
592 let mut line = String::with_capacity(2 + message.len());
593 line.push_str("● ");
594 line.push_str(message);
595 ui.styled(line, Style::new().fg(*color));
596 }
597 });
598 });
599 });
600 }
601}