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