1use crate::model::event::{BufferId, LeafId, SplitId};
13
14use super::Editor;
15
16pub(super) fn render_floating_spec(
24 focus_marker: bool,
25 spec: &fresh_core::api::WidgetSpec,
26 prev: &std::collections::HashMap<String, crate::widgets::WidgetInstanceState>,
27 prev_focus_key: &str,
28 panel_width: u32,
29) -> crate::widgets::RenderOutput {
30 if focus_marker {
31 crate::widgets::render_spec_with_marker(spec, prev, prev_focus_key, panel_width)
32 } else {
33 crate::widgets::render_spec(spec, prev, prev_focus_key, panel_width)
34 }
35}
36
37fn find_scrollable_widget_key(spec: &fresh_core::api::WidgetSpec) -> Option<String> {
45 use fresh_core::api::WidgetSpec;
46 match spec {
47 WidgetSpec::Tree { key: Some(k), .. } | WidgetSpec::List { key: Some(k), .. }
48 if !k.is_empty() =>
49 {
50 return Some(k.clone());
51 }
52 _ => {}
53 }
54 spec.children().find_map(find_scrollable_widget_key)
55}
56
57fn collect_visible_tree_indices(
58 nodes: &[fresh_core::api::TreeNode],
59 item_keys: &[String],
60 expanded: &std::collections::HashSet<String>,
61) -> Vec<usize> {
62 let mut ancestor_open: Vec<bool> = Vec::new();
63 let mut visible: Vec<usize> = Vec::with_capacity(nodes.len());
64 for (i, node) in nodes.iter().enumerate() {
65 let depth = node.depth as usize;
66 ancestor_open.truncate(depth);
67 if ancestor_open.iter().all(|open| *open) {
68 visible.push(i);
69 }
70 let key = item_keys.get(i).cloned().unwrap_or_default();
71 let is_open = if node.has_children {
72 !key.is_empty() && expanded.contains(&key)
73 } else {
74 true
75 };
76 ancestor_open.push(is_open);
77 }
78 visible
79}
80
81pub(super) fn translate_plugin_animation_kind(
84 kind: fresh_core::api::PluginAnimationKind,
85) -> crate::view::animation::AnimationKind {
86 use crate::view::animation::{AnimationKind, Edge};
87 use fresh_core::api::{PluginAnimationEdge, PluginAnimationKind};
88 use std::time::Duration;
89 match kind {
90 PluginAnimationKind::SlideIn {
91 from,
92 duration_ms,
93 delay_ms,
94 } => AnimationKind::SlideIn {
95 from: match from {
96 PluginAnimationEdge::Top => Edge::Top,
97 PluginAnimationEdge::Bottom => Edge::Bottom,
98 PluginAnimationEdge::Left => Edge::Left,
99 PluginAnimationEdge::Right => Edge::Right,
100 },
101 duration: Duration::from_millis(duration_ms as u64),
102 delay: Duration::from_millis(delay_ms as u64),
103 },
104 }
105}
106
107impl Editor {
108 pub(crate) fn deliver_widget_hit(
114 &mut self,
115 panel_key: &crate::widgets::PanelKey,
116 hit: &crate::widgets::HitArea,
117 ) {
118 if !hit.widget_key.is_empty() {
121 let is_tabbable = self
122 .widget_registry
123 .get(panel_key)
124 .map(|p| p.tabbable.iter().any(|k| k == &hit.widget_key))
125 .unwrap_or(false);
126 if is_tabbable {
127 self.set_panel_focus_and_notify(panel_key, hit.widget_key.clone());
128 }
129 self.rerender_widget_panel(panel_key);
130 }
131 let mut handled_specially = false;
135 if hit.widget_kind == "tree" && hit.event_type == "expand" {
136 if let Some(item_key) = hit.payload.get("key").and_then(|v| v.as_str()) {
137 self.handle_widget_tree_expand_toggle(panel_key, &hit.widget_key, item_key);
138 handled_specially = true;
139 }
140 }
141 let mut event_widget_key = hit.widget_key.clone();
146 if hit.widget_kind == "list" && hit.event_type == "select" {
147 if let Some(list_key) = hit.payload.get("list_key").and_then(|v| v.as_str()) {
148 event_widget_key = list_key.to_string();
149 if let Some(idx) = hit.payload.get("index").and_then(|v| v.as_i64()) {
150 self.set_widget_list_selected_index(panel_key, list_key, idx as i32);
151 }
152 }
153 }
154 if !handled_specially {
155 self.fire_widget_event(
156 panel_key,
157 event_widget_key,
158 hit.event_type.to_string(),
159 hit.payload.clone(),
160 );
161 }
162 }
163
164 pub fn deliver_widget_hit_by_index(&mut self, plugin: &str, panel_id: u64, hit_index: usize) {
168 let panel_key = crate::widgets::PanelKey::new(plugin, panel_id);
169 let hit = self
170 .widget_registry
171 .get(&panel_key)
172 .and_then(|p| p.hits.get(hit_index).cloned());
173 if let Some(hit) = hit {
174 self.deliver_widget_hit(&panel_key, &hit);
175 }
176 }
177
178 pub(crate) fn fire_widget_event(
182 &self,
183 panel_key: &crate::widgets::PanelKey,
184 widget_key: String,
185 event_type: String,
186 payload: serde_json::Value,
187 ) {
188 let pm = self.plugin_manager.read().unwrap();
189 if !pm.has_hook_handlers("widget_event") {
190 return;
191 }
192 pm.run_hook_for_plugin(
193 &panel_key.plugin,
194 "widget_event",
195 fresh_core::hooks::HookArgs::WidgetEvent {
196 panel_id: panel_key.id,
197 widget_key,
198 event_type,
199 payload,
200 },
201 );
202 }
203
204 pub(super) fn apply_widget_focus_cursor(
215 &mut self,
216 buffer_id: BufferId,
217 entries: &[fresh_core::text_property::TextPropertyEntry],
218 focus_cursor: Option<crate::widgets::FocusCursor>,
219 ) {
220 let locked = self
226 .windows
227 .get(&self.active_window)
228 .and_then(|w| w.buffers.get(&buffer_id))
229 .map(|s| s.cursor_visibility_locked)
230 .unwrap_or(false);
231 if locked {
232 return;
233 }
234
235 let absolute_byte = focus_cursor.map(|fc| {
236 let row = fc.buffer_row as usize;
237 let prefix: usize = entries.iter().take(row).map(|e| e.text.len()).sum();
238 prefix + fc.byte_in_row as usize
239 });
240
241 if let Some(state) = self
242 .windows
243 .get_mut(&self.active_window)
244 .map(|w| &mut w.buffers)
245 .expect("active window present")
246 .get_mut(&buffer_id)
247 {
248 state.show_cursors = absolute_byte.is_some();
249 }
250
251 if let Some(byte) = absolute_byte {
252 for vs in self
253 .windows
254 .get_mut(&self.active_window)
255 .and_then(|w| w.split_view_states_mut())
256 .expect("active window must have a populated split layout")
257 .values_mut()
258 {
259 if vs.buffer_state(buffer_id).is_some() {
260 let cursor = vs.cursors.primary_mut();
261 cursor.position = byte;
262 }
263 }
264 }
265 }
266
267 pub(super) fn widget_panel_width(&self, buffer_id: BufferId) -> u32 {
276 let raw = self
277 .windows
278 .get(&self.active_window)
279 .and_then(|w| w.buffers.splits())
280 .map(|(_, vs)| vs)
281 .expect("active window must have a populated split layout")
282 .values()
283 .find(|vs| vs.buffer_state(buffer_id).is_some() && vs.viewport.width > 0)
284 .map(|vs| vs.viewport.width as u32)
285 .unwrap_or_else(|| self.terminal_width.max(1) as u32);
286 raw.saturating_sub(2).max(10)
289 }
290
291 pub(super) fn rerender_widget_panel(&mut self, panel_key: &crate::widgets::PanelKey) {
297 let (buffer_id, _is_floating, panel_width, out_pieces) = {
306 let (buffer_id, spec) = match self.widget_registry.buffer_and_spec_ref(panel_key) {
307 Some(s) => s,
308 None => return,
309 };
310 let prev = self
311 .widget_registry
312 .instance_states(panel_key)
313 .cloned()
314 .unwrap_or_default();
315 let prev_focus = self
316 .widget_registry
317 .focus_key(panel_key)
318 .map(|s| s.to_string())
319 .unwrap_or_default();
320 let panel_slot = Self::slot_for_panel_buffer(buffer_id);
321 let is_floating = panel_slot.is_some();
322 let panel_width = if let Some(slot) = panel_slot {
323 self.floating_panel_inner_width(slot)
324 } else {
325 self.widget_panel_width(buffer_id)
326 };
327 let focus_marker = panel_slot
333 .and_then(|slot| self.panel(slot))
334 .map(|f| f.focus_marker)
335 .unwrap_or(false);
336 let out = render_floating_spec(focus_marker, spec, &prev, &prev_focus, panel_width);
337 (buffer_id, is_floating, panel_width, out)
338 };
339 let _ = panel_width;
340 let panel_slot = Self::slot_for_panel_buffer(buffer_id);
341 let focus_cursor = out_pieces.focus_cursor;
342 let entries = out_pieces.entries;
343 let embeds = out_pieces.embeds;
344 let overlays = out_pieces.overlays;
345 let scroll_regions = out_pieces.scroll_regions;
346 if self
347 .widget_registry
348 .update_side_effects(
349 panel_key,
350 out_pieces.hits,
351 out_pieces.instance_states,
352 out_pieces.focus_key,
353 out_pieces.tabbable,
354 )
355 .is_err()
356 {
357 tracing::warn!("rerender_widget_panel({}) lost panel mid-call", panel_key);
358 return;
359 }
360 if let Some(slot) = panel_slot {
361 if let Some(fwp) = self.panel_mut(slot) {
362 if &fwp.panel_key == panel_key {
363 fwp.entries = entries;
364 fwp.focus_cursor = focus_cursor;
365 fwp.embeds = embeds;
366 fwp.overlays = overlays;
367 fwp.scroll_regions = scroll_regions;
368 }
369 }
370 return;
371 }
372 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
373 tracing::error!("rerender_widget_panel({}) failed: {}", panel_key, e);
374 }
375 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
376 }
377
378 pub(super) fn handle_widget_command(
379 &mut self,
380 panel_key: &crate::widgets::PanelKey,
381 action: fresh_core::api::WidgetAction,
382 ) {
383 use fresh_core::api::WidgetAction;
384 match action {
385 WidgetAction::FocusAdvance { delta } => {
386 self.handle_widget_focus_advance(panel_key, delta);
387 }
388 WidgetAction::Activate => {
389 self.handle_widget_activate(panel_key);
390 }
391 WidgetAction::SelectMove { delta } => {
392 self.handle_widget_select_move(panel_key, delta);
393 }
394 WidgetAction::TextInputKey { key } => {
395 self.handle_widget_text_key(panel_key, &key);
396 }
397 WidgetAction::TextInputChar { text } => {
398 self.handle_widget_text_char(panel_key, &text);
399 }
400 WidgetAction::Key { key } => {
401 self.handle_widget_key(panel_key, &key);
402 }
403 }
404 }
405
406 fn handle_widget_key(&mut self, panel_key: &crate::widgets::PanelKey, key: &str) {
407 let panel = match self.widget_registry.get(panel_key) {
411 Some(p) => p,
412 None => return,
413 };
414 let focus_key = panel.focus_key.clone();
415 let completions_open = matches!(key, "Tab" | "Up" | "Down" | "Enter" | "Escape")
425 && self.focused_text_completions_open(panel_key);
426 if completions_open {
427 match key {
428 "Up" => {
429 self.move_focused_text_completion_index(panel_key, -1);
430 self.rerender_widget_panel(panel_key);
435 return;
436 }
437 "Down" => {
438 self.move_focused_text_completion_index(panel_key, 1);
439 self.rerender_widget_panel(panel_key);
440 return;
441 }
442 "Escape" => {
443 self.dismiss_focused_text_completions(panel_key);
446 self.rerender_widget_panel(panel_key);
447 return;
448 }
449 "Enter" => {
450 if self.focused_text_completion_navigated(panel_key) {
451 self.fire_completion_accept(panel_key);
454 return;
455 }
456 self.dismiss_focused_text_completions(panel_key);
460 }
461 "Tab" => {
462 if self.focused_text_completion_navigated(panel_key) {
463 self.fire_completion_accept(panel_key);
469 return;
470 }
471 self.dismiss_focused_text_completions(panel_key);
476 }
477 _ => {}
478 }
479 }
480 let panel = match self.widget_registry.get(panel_key) {
485 Some(p) => p,
486 None => return,
487 };
488 let widget = if focus_key.is_empty() {
489 None
490 } else {
491 crate::widgets::find_widget_by_key(&panel.spec, &focus_key)
492 };
493 match key {
494 "Tab" => self.handle_widget_focus_advance(panel_key, 1),
495 "Shift+Tab" => self.handle_widget_focus_advance(panel_key, -1),
496 "Up" | "Down" => {
497 let delta = if key == "Up" { -1 } else { 1 };
498 match widget {
499 Some(fresh_core::api::WidgetSpec::List { .. }) => {
500 self.handle_widget_select_move(panel_key, delta);
501 }
502 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
503 self.handle_widget_tree_select_move(panel_key, delta);
504 }
505 Some(fresh_core::api::WidgetSpec::Text { rows, .. }) if *rows > 1 => {
506 self.handle_widget_text_key(panel_key, key);
512 }
513 _ => {
514 let scrollable = self
522 .widget_registry
523 .get(panel_key)
524 .and_then(|p| find_scrollable_widget_key(&p.spec));
525 if let Some(target_key) = scrollable {
526 let target_kind = self.widget_registry.get(panel_key).and_then(|p| {
527 crate::widgets::find_widget_by_key(&p.spec, &target_key).cloned()
528 });
529 match target_kind {
530 Some(fresh_core::api::WidgetSpec::List { .. }) => {
531 self.handle_widget_select_move_for_key(
532 panel_key,
533 &target_key,
534 delta,
535 );
536 }
537 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
538 self.handle_widget_tree_select_move_for_key(
539 panel_key,
540 &target_key,
541 delta,
542 );
543 }
544 _ => {}
545 }
546 }
547 }
548 }
549 }
550 "PageUp" | "PageDown" => {
551 let page = match widget {
555 Some(fresh_core::api::WidgetSpec::List { visible_rows, .. })
556 | Some(fresh_core::api::WidgetSpec::Tree { visible_rows, .. }) => {
557 visible_rows.saturating_sub(1).max(1) as i32
558 }
559 _ => 0,
560 };
561 if page == 0 {
562 return;
563 }
564 let delta = if key == "PageUp" { -page } else { page };
565 match widget {
566 Some(fresh_core::api::WidgetSpec::List { .. }) => {
567 self.handle_widget_select_move(panel_key, delta);
568 }
569 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
570 self.handle_widget_tree_select_move(panel_key, delta);
571 }
572 _ => {}
573 }
574 }
575 "Left" | "Right" => match widget {
576 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
577 self.handle_widget_text_key(panel_key, key);
578 }
579 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
580 self.handle_widget_tree_lateral(panel_key, key == "Right");
581 }
582 _ => {}
583 },
584 "Backspace" | "Delete" | "Home" | "End" => match widget {
585 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
586 self.handle_widget_text_key(panel_key, key);
587 }
588 _ => {}
589 },
590 "Enter" => match widget {
591 Some(fresh_core::api::WidgetSpec::Button { .. })
592 | Some(fresh_core::api::WidgetSpec::Toggle { .. }) => {
593 self.handle_widget_activate(panel_key);
594 }
595 Some(fresh_core::api::WidgetSpec::List { .. }) => {
596 self.fire_list_activate(panel_key, &focus_key);
597 }
598 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
599 self.fire_tree_activate(panel_key, &focus_key);
600 }
601 Some(fresh_core::api::WidgetSpec::Text { rows, .. }) => {
602 if *rows > 1 {
603 self.handle_widget_text_key(panel_key, "Enter");
609 } else if let Some(target_key) = self
610 .widget_registry
611 .get(panel_key)
612 .and_then(|p| find_scrollable_widget_key(&p.spec))
613 {
614 let kind = self.widget_registry.get(panel_key).and_then(|p| {
620 crate::widgets::find_widget_by_key(&p.spec, &target_key).cloned()
621 });
622 match kind {
623 Some(fresh_core::api::WidgetSpec::List { .. }) => {
624 self.fire_list_activate(panel_key, &target_key);
625 }
626 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
627 self.fire_tree_activate(panel_key, &target_key);
628 }
629 _ => {}
630 }
631 } else {
632 self.handle_widget_focus_advance(panel_key, 1);
635 }
636 }
637 _ => {}
638 },
639 "Space" => match widget {
640 Some(fresh_core::api::WidgetSpec::Button { .. })
641 | Some(fresh_core::api::WidgetSpec::Toggle { .. }) => {
642 self.handle_widget_activate(panel_key);
643 }
644 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
645 self.handle_widget_text_char(panel_key, " ");
646 }
647 Some(fresh_core::api::WidgetSpec::List { .. }) => {
648 self.fire_list_activate(panel_key, &focus_key);
649 }
650 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
651 if !self.fire_tree_toggle_if_checkable(panel_key, &focus_key) {
658 self.fire_tree_activate(panel_key, &focus_key);
659 }
660 }
661 _ => {}
662 },
663 _ => {} }
665 }
666
667 fn handle_widget_focus_advance(&mut self, panel_key: &crate::widgets::PanelKey, delta: i32) {
668 let panel = match self.widget_registry.get(panel_key) {
669 Some(p) => p,
670 None => return,
671 };
672 if panel.tabbable.is_empty() {
673 return;
674 }
675 let cur_idx = panel
676 .tabbable
677 .iter()
678 .position(|k| k == &panel.focus_key)
679 .unwrap_or(0) as i32;
680 let n = panel.tabbable.len() as i32;
681 let new_idx = ((cur_idx + delta) % n + n) % n;
682 let new_key = panel.tabbable[new_idx as usize].clone();
683 self.set_panel_focus_and_notify(panel_key, new_key);
684 self.rerender_widget_panel(panel_key);
685 }
686
687 pub(crate) fn set_panel_focus_and_notify(
697 &mut self,
698 panel_key: &crate::widgets::PanelKey,
699 new_key: String,
700 ) {
701 let old_key = self
702 .widget_registry
703 .focus_key(panel_key)
704 .map(|s| s.to_string())
705 .unwrap_or_default();
706 if old_key == new_key {
707 tracing::debug!(
708 target: "fresh::dock",
709 panel = %panel_key,
710 key = %new_key,
711 "set_panel_focus_and_notify: no-op (old == new)"
712 );
713 return;
714 }
715 tracing::debug!(
716 target: "fresh::dock",
717 panel = %panel_key,
718 old = %old_key,
719 new = %new_key,
720 "set_panel_focus_and_notify: firing `focus` widget_event"
721 );
722 self.widget_registry
723 .set_focus_key(panel_key, new_key.clone());
724 self.fire_widget_event(
725 panel_key,
726 new_key,
727 "focus".to_string(),
728 serde_json::json!({ "previous": old_key }),
729 );
730 }
731
732 fn handle_widget_activate(&mut self, panel_key: &crate::widgets::PanelKey) {
733 let panel = match self.widget_registry.get(panel_key) {
737 Some(p) => p,
738 None => return,
739 };
740 let focus_key = panel.focus_key.clone();
741 if focus_key.is_empty() {
742 return;
743 }
744 let widget = crate::widgets::find_widget_by_key(&panel.spec, &focus_key);
745 let (event_type, payload) = match widget {
746 Some(fresh_core::api::WidgetSpec::Button { disabled: true, .. }) => return,
753 Some(fresh_core::api::WidgetSpec::Button { .. }) => ("activate", serde_json::json!({})),
754 Some(fresh_core::api::WidgetSpec::Toggle { checked, .. }) => {
755 ("toggle", serde_json::json!({ "checked": !checked }))
756 }
757 _ => return,
758 };
759 self.fire_widget_event(panel_key, focus_key, event_type.to_string(), payload);
760 }
761
762 fn focused_text_completions_open(&self, panel_key: &crate::widgets::PanelKey) -> bool {
774 let panel = match self.widget_registry.get(panel_key) {
775 Some(p) => p,
776 None => return false,
777 };
778 if panel.focus_key.is_empty() {
779 return false;
780 }
781 matches!(
782 panel.instance_states.get(&panel.focus_key),
783 Some(crate::widgets::WidgetInstanceState::Text { completions, .. })
784 if !completions.is_empty()
785 )
786 }
787
788 fn focused_text_completion_navigated(&self, panel_key: &crate::widgets::PanelKey) -> bool {
793 let panel = match self.widget_registry.get(panel_key) {
794 Some(p) => p,
795 None => return false,
796 };
797 if panel.focus_key.is_empty() {
798 return false;
799 }
800 matches!(
801 panel.instance_states.get(&panel.focus_key),
802 Some(crate::widgets::WidgetInstanceState::Text {
803 completions,
804 completion_navigated,
805 ..
806 }) if !completions.is_empty() && *completion_navigated
807 )
808 }
809
810 fn move_focused_text_completion_index(
821 &mut self,
822 panel_key: &crate::widgets::PanelKey,
823 delta: i32,
824 ) {
825 let panel = match self.widget_registry.get(panel_key) {
832 Some(p) => p,
833 None => return,
834 };
835 let focus_key = panel.focus_key.clone();
836 if focus_key.is_empty() {
837 return;
838 }
839 let spec_visible_rows = match crate::widgets::find_widget_by_key(&panel.spec, &focus_key) {
840 Some(fresh_core::api::WidgetSpec::Text {
841 completions_visible_rows,
842 ..
843 }) => *completions_visible_rows,
844 _ => 0,
845 };
846 let visible = if spec_visible_rows == 0 {
847 5u32
848 } else {
849 spec_visible_rows
850 };
851 let panel = match self.widget_registry.get_mut(panel_key) {
852 Some(p) => p,
853 None => return,
854 };
855 if let Some(crate::widgets::WidgetInstanceState::Text {
856 completions,
857 completion_selected_index,
858 completion_scroll_offset,
859 completion_navigated,
860 ..
861 }) = panel.instance_states.get_mut(&focus_key)
862 {
863 if completions.is_empty() {
864 return;
865 }
866 if !*completion_navigated {
871 *completion_navigated = true;
872 return;
873 }
874 let max = (completions.len() - 1) as i32;
875 let cur = *completion_selected_index as i32;
876 let next = (cur + delta).clamp(0, max);
877 *completion_selected_index = next as usize;
878 let next_u = next as u32;
883 if next_u < *completion_scroll_offset {
884 *completion_scroll_offset = next_u;
885 } else if next_u >= *completion_scroll_offset + visible {
886 *completion_scroll_offset = next_u + 1 - visible;
887 }
888 }
889 }
890
891 fn dismiss_focused_text_completions(&mut self, panel_key: &crate::widgets::PanelKey) {
898 let focus_key = {
899 let panel = match self.widget_registry.get_mut(panel_key) {
900 Some(p) => p,
901 None => return,
902 };
903 let focus_key = panel.focus_key.clone();
904 if focus_key.is_empty() {
905 return;
906 }
907 if let Some(crate::widgets::WidgetInstanceState::Text {
908 completions,
909 completion_selected_index,
910 ..
911 }) = panel.instance_states.get_mut(&focus_key)
912 {
913 if completions.is_empty() {
914 return;
915 }
916 completions.clear();
917 *completion_selected_index = 0;
918 } else {
919 return;
920 }
921 focus_key
922 };
923 self.fire_widget_event(
924 panel_key,
925 focus_key,
926 "completion_dismiss".into(),
927 serde_json::json!({}),
928 );
929 }
930
931 fn fire_completion_accept(&mut self, panel_key: &crate::widgets::PanelKey) {
943 let (focus_key, value) = {
944 let panel = match self.widget_registry.get(panel_key) {
945 Some(p) => p,
946 None => return,
947 };
948 let focus_key = panel.focus_key.clone();
949 if focus_key.is_empty() {
950 return;
951 }
952 match panel.instance_states.get(&focus_key) {
953 Some(crate::widgets::WidgetInstanceState::Text {
954 completions,
955 completion_selected_index,
956 ..
957 }) if !completions.is_empty() => {
958 let idx = (*completion_selected_index).min(completions.len() - 1);
959 (focus_key, completions[idx].value.clone())
960 }
961 _ => return,
962 }
963 };
964 self.fire_widget_event(
965 panel_key,
966 focus_key,
967 "completion_accept".into(),
968 serde_json::json!({ "value": value }),
969 );
970 }
971
972 fn fire_list_activate(&mut self, panel_key: &crate::widgets::PanelKey, focus_key: &str) {
973 let panel = match self.widget_registry.get(panel_key) {
974 Some(p) => p,
975 None => return,
976 };
977 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
978 let (spec_sel, item_keys) = match widget {
979 Some(fresh_core::api::WidgetSpec::List {
980 selected_index,
981 item_keys,
982 ..
983 }) => (*selected_index, item_keys.clone()),
984 _ => return,
985 };
986 let sel = match panel.instance_states.get(focus_key) {
987 Some(crate::widgets::WidgetInstanceState::List { selected_index, .. }) => {
988 *selected_index
989 }
990 _ => spec_sel,
991 };
992 if sel < 0 {
993 return;
994 }
995 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
996 self.fire_widget_event(
997 panel_key,
998 focus_key.to_string(),
999 "activate".into(),
1000 serde_json::json!({ "index": sel, "key": item_key, }),
1001 );
1002 }
1003
1004 fn handle_widget_select_move(&mut self, panel_key: &crate::widgets::PanelKey, delta: i32) {
1005 let focus_key = match self.widget_registry.get(panel_key) {
1006 Some(p) => p.focus_key.clone(),
1007 None => return,
1008 };
1009 if focus_key.is_empty() {
1010 return;
1011 }
1012 self.handle_widget_select_move_for_key(panel_key, &focus_key, delta);
1013 }
1014
1015 pub(super) fn set_widget_list_selected_index(
1023 &mut self,
1024 panel_key: &crate::widgets::PanelKey,
1025 widget_key: &str,
1026 index: i32,
1027 ) {
1028 if let Some(panel) = self.widget_registry.get_mut(panel_key) {
1029 let (prev_scroll, prev_item_height) = match panel.instance_states.get(widget_key) {
1030 Some(crate::widgets::WidgetInstanceState::List {
1031 scroll_offset,
1032 item_height,
1033 ..
1034 }) => (*scroll_offset, *item_height),
1035 _ => (0, 1),
1036 };
1037 panel.instance_states.insert(
1038 widget_key.to_string(),
1039 crate::widgets::WidgetInstanceState::List {
1040 scroll_offset: prev_scroll,
1041 selected_index: index,
1042 item_height: prev_item_height,
1043 user_scrolled: false,
1045 },
1046 );
1047 }
1048 self.rerender_widget_panel(panel_key);
1049 }
1050
1051 fn handle_widget_select_move_for_key(
1057 &mut self,
1058 panel_key: &crate::widgets::PanelKey,
1059 widget_key: &str,
1060 delta: i32,
1061 ) {
1062 let panel = match self.widget_registry.get(panel_key) {
1063 Some(p) => p,
1064 None => return,
1065 };
1066 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
1067 let (spec_sel, total, item_keys) = match widget {
1068 Some(fresh_core::api::WidgetSpec::List {
1069 selected_index,
1070 items,
1071 item_specs,
1072 item_keys,
1073 ..
1074 }) => {
1075 let total = if item_specs.is_empty() {
1078 items.len()
1079 } else {
1080 item_specs.len()
1081 };
1082 (*selected_index, total as i32, item_keys.clone())
1083 }
1084 _ => return,
1085 };
1086 if total == 0 {
1087 return;
1088 }
1089 let cur_sel = match panel.instance_states.get(widget_key) {
1090 Some(crate::widgets::WidgetInstanceState::List { selected_index, .. }) => {
1091 *selected_index
1092 }
1093 _ => spec_sel,
1094 };
1095 let raw = if cur_sel < 0 { 0 } else { cur_sel + delta };
1096 let new_sel = raw.clamp(0, total - 1);
1097 let new_key = item_keys.get(new_sel as usize).cloned().unwrap_or_default();
1098 if let Some(panel_mut) = self.widget_registry.get_mut(panel_key) {
1099 let (cur_scroll, cur_item_height) = match panel_mut.instance_states.get(widget_key) {
1100 Some(crate::widgets::WidgetInstanceState::List {
1101 scroll_offset,
1102 item_height,
1103 ..
1104 }) => (*scroll_offset, *item_height),
1105 _ => (0, 1),
1106 };
1107 panel_mut.instance_states.insert(
1108 widget_key.to_string(),
1109 crate::widgets::WidgetInstanceState::List {
1110 scroll_offset: cur_scroll,
1111 selected_index: new_sel,
1112 item_height: cur_item_height,
1113 user_scrolled: false,
1116 },
1117 );
1118 }
1119 self.rerender_widget_panel(panel_key);
1120 if new_sel != cur_sel {
1130 self.fire_widget_event(
1131 panel_key,
1132 widget_key.to_string(),
1133 "select".into(),
1134 serde_json::json!({ "index": new_sel, "key": new_key }),
1135 );
1136 }
1137 }
1138
1139 fn handle_widget_tree_select_move(&mut self, panel_key: &crate::widgets::PanelKey, delta: i32) {
1144 let focus_key = match self.widget_registry.get(panel_key) {
1145 Some(p) => p.focus_key.clone(),
1146 None => return,
1147 };
1148 if focus_key.is_empty() {
1149 return;
1150 }
1151 self.handle_widget_tree_select_move_for_key(panel_key, &focus_key, delta);
1152 }
1153
1154 fn handle_widget_tree_select_move_for_key(
1156 &mut self,
1157 panel_key: &crate::widgets::PanelKey,
1158 widget_key: &str,
1159 delta: i32,
1160 ) {
1161 let panel = match self.widget_registry.get(panel_key) {
1162 Some(p) => p,
1163 None => return,
1164 };
1165 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
1166 let (spec_sel, nodes, item_keys) = match widget {
1167 Some(fresh_core::api::WidgetSpec::Tree {
1168 selected_index,
1169 nodes,
1170 item_keys,
1171 ..
1172 }) => (*selected_index, nodes.clone(), item_keys.clone()),
1173 _ => return,
1174 };
1175 if nodes.is_empty() {
1176 return;
1177 }
1178 let (cur_sel, cur_scroll, expanded) = match panel.instance_states.get(widget_key) {
1179 Some(crate::widgets::WidgetInstanceState::Tree {
1180 selected_index,
1181 scroll_offset,
1182 expanded_keys,
1183 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
1184 _ => (spec_sel, 0u32, std::collections::HashSet::<String>::new()),
1185 };
1186 let visible_indices = collect_visible_tree_indices(&nodes, &item_keys, &expanded);
1187 if visible_indices.is_empty() {
1188 return;
1189 }
1190 let cur_pos = if cur_sel < 0 {
1191 if delta > 0 {
1192 -1
1193 } else {
1194 visible_indices.len() as i32
1195 }
1196 } else {
1197 visible_indices
1198 .iter()
1199 .position(|&v| v as i32 == cur_sel)
1200 .map(|p| p as i32)
1201 .unwrap_or(-1)
1202 };
1203 let new_pos = (cur_pos + delta).clamp(0, (visible_indices.len() as i32) - 1);
1204 let new_abs = visible_indices[new_pos as usize];
1205 let new_key = item_keys.get(new_abs).cloned().unwrap_or_default();
1206 if let Some(panel_mut) = self.widget_registry.get_mut(panel_key) {
1207 panel_mut.instance_states.insert(
1208 widget_key.to_string(),
1209 crate::widgets::WidgetInstanceState::Tree {
1210 scroll_offset: cur_scroll,
1211 selected_index: new_abs as i32,
1212 expanded_keys: expanded,
1213 },
1214 );
1215 }
1216 self.rerender_widget_panel(panel_key);
1217 self.fire_widget_event(
1218 panel_key,
1219 widget_key.to_string(),
1220 "select".into(),
1221 serde_json::json!({ "index": new_abs as i64, "key": new_key }),
1222 );
1223 }
1224
1225 pub(super) fn handle_widget_panel_wheel(
1235 &mut self,
1236 buffer_id: crate::model::event::BufferId,
1237 delta: i32,
1238 ) -> bool {
1239 let panels = self.widget_registry.panels_for_buffer(buffer_id);
1240 let mut consumed = false;
1241 for panel_key in panels {
1242 if self.focused_text_completions_open(&panel_key) {
1248 self.scroll_focused_text_completions(&panel_key, delta);
1249 self.rerender_widget_panel(&panel_key);
1258 consumed = true;
1259 continue;
1260 }
1261 let spec = match self.widget_registry.get(&panel_key) {
1262 Some(p) => p.spec.clone(),
1263 None => continue,
1264 };
1265 let Some(widget_key) = find_scrollable_widget_key(&spec) else {
1266 continue;
1267 };
1268 let widget = crate::widgets::find_widget_by_key(&spec, &widget_key);
1269 match widget {
1270 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
1271 consumed |= self.handle_widget_tree_wheel(&panel_key, &widget_key, delta);
1279 }
1280 Some(fresh_core::api::WidgetSpec::List { .. }) => {
1281 consumed |= self.handle_widget_list_wheel(&panel_key, &widget_key, delta);
1282 }
1283 _ => {}
1284 }
1285 }
1286 consumed
1287 }
1288
1289 fn scroll_focused_text_completions(
1296 &mut self,
1297 panel_key: &crate::widgets::PanelKey,
1298 delta: i32,
1299 ) {
1300 let panel = match self.widget_registry.get(panel_key) {
1301 Some(p) => p,
1302 None => return,
1303 };
1304 let focus_key = panel.focus_key.clone();
1305 if focus_key.is_empty() {
1306 return;
1307 }
1308 let spec_visible_rows = match crate::widgets::find_widget_by_key(&panel.spec, &focus_key) {
1309 Some(fresh_core::api::WidgetSpec::Text {
1310 completions_visible_rows,
1311 ..
1312 }) => *completions_visible_rows,
1313 _ => 0,
1314 };
1315 let visible = if spec_visible_rows == 0 {
1316 5u32
1317 } else {
1318 spec_visible_rows
1319 };
1320 let panel = match self.widget_registry.get_mut(panel_key) {
1321 Some(p) => p,
1322 None => return,
1323 };
1324 if let Some(crate::widgets::WidgetInstanceState::Text {
1325 completions,
1326 completion_scroll_offset,
1327 completion_navigated,
1328 ..
1329 }) = panel.instance_states.get_mut(&focus_key)
1330 {
1331 if completions.is_empty() {
1332 return;
1333 }
1334 *completion_navigated = true;
1337 let total = completions.len() as u32;
1338 let max_scroll = total.saturating_sub(visible.min(total));
1339 let next = (*completion_scroll_offset as i32 + delta).clamp(0, max_scroll as i32);
1340 *completion_scroll_offset = next as u32;
1341 }
1342 }
1343
1344 fn handle_widget_tree_wheel(
1349 &mut self,
1350 panel_key: &crate::widgets::PanelKey,
1351 widget_key: &str,
1352 delta: i32,
1353 ) -> bool {
1354 let panel = match self.widget_registry.get(panel_key) {
1355 Some(p) => p,
1356 None => return false,
1357 };
1358 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
1359 let (visible_rows, nodes, item_keys) = match widget {
1360 Some(fresh_core::api::WidgetSpec::Tree {
1361 visible_rows,
1362 nodes,
1363 item_keys,
1364 ..
1365 }) => (*visible_rows, nodes.clone(), item_keys.clone()),
1366 _ => return false,
1367 };
1368 if nodes.is_empty() {
1369 return false;
1370 }
1371 let (cur_sel, cur_scroll, expanded) = match panel.instance_states.get(widget_key) {
1372 Some(crate::widgets::WidgetInstanceState::Tree {
1373 selected_index,
1374 scroll_offset,
1375 expanded_keys,
1376 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
1377 _ => (-1, 0, std::collections::HashSet::<String>::new()),
1378 };
1379 let visible_indices = collect_visible_tree_indices(&nodes, &item_keys, &expanded);
1380 if visible_indices.is_empty() {
1381 return false;
1382 }
1383 let visible = visible_rows.max(1);
1384 let total_visible = visible_indices.len() as u32;
1385 let max_scroll = total_visible.saturating_sub(visible);
1386 let new_scroll = (cur_scroll as i32 + delta).clamp(0, max_scroll as i32) as u32;
1387 if new_scroll == cur_scroll {
1388 return false;
1389 }
1390 let cur_pos: Option<u32> = if cur_sel >= 0 {
1392 visible_indices
1393 .iter()
1394 .position(|&v| v as i32 == cur_sel)
1395 .map(|p| p as u32)
1396 } else {
1397 None
1398 };
1399 let new_sel_abs = match cur_pos {
1400 Some(pos) if pos < new_scroll => visible_indices[new_scroll as usize] as i32,
1401 Some(pos) if pos >= new_scroll + visible => {
1402 visible_indices[(new_scroll + visible - 1) as usize] as i32
1403 }
1404 _ => cur_sel,
1405 };
1406 if let Some(panel_mut) = self.widget_registry.get_mut(panel_key) {
1407 panel_mut.instance_states.insert(
1408 widget_key.to_string(),
1409 crate::widgets::WidgetInstanceState::Tree {
1410 scroll_offset: new_scroll,
1411 selected_index: new_sel_abs,
1412 expanded_keys: expanded,
1413 },
1414 );
1415 }
1416 self.rerender_widget_panel(panel_key);
1417 true
1418 }
1419
1420 fn handle_widget_list_wheel(
1423 &mut self,
1424 panel_key: &crate::widgets::PanelKey,
1425 widget_key: &str,
1426 delta: i32,
1427 ) -> bool {
1428 let panel = match self.widget_registry.get(panel_key) {
1429 Some(p) => p,
1430 None => return false,
1431 };
1432 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
1433 let (visible_rows, total) = match widget {
1434 Some(fresh_core::api::WidgetSpec::List {
1435 visible_rows,
1436 items,
1437 item_specs,
1438 ..
1439 }) => {
1440 let total = if item_specs.is_empty() {
1441 items.len()
1442 } else {
1443 item_specs.len()
1444 };
1445 (*visible_rows, total as u32)
1446 }
1447 _ => return false,
1448 };
1449 if total == 0 {
1450 return false;
1451 }
1452 let (cur_sel, cur_scroll, item_height) = match panel.instance_states.get(widget_key) {
1453 Some(crate::widgets::WidgetInstanceState::List {
1454 selected_index,
1455 scroll_offset,
1456 item_height,
1457 ..
1458 }) => (*selected_index, *scroll_offset, (*item_height).max(1)),
1459 _ => (-1, 0, 1),
1460 };
1461 let visible_items = (visible_rows.max(1) / item_height).max(1);
1468 let max_scroll = total.saturating_sub(visible_items);
1469 let new_scroll = (cur_scroll as i64 + delta as i64).clamp(0, max_scroll as i64) as u32;
1470 if new_scroll == cur_scroll {
1471 return false;
1472 }
1473 if let Some(panel_mut) = self.widget_registry.get_mut(panel_key) {
1477 panel_mut.instance_states.insert(
1478 widget_key.to_string(),
1479 crate::widgets::WidgetInstanceState::List {
1480 scroll_offset: new_scroll,
1481 selected_index: cur_sel,
1482 item_height,
1483 user_scrolled: true,
1484 },
1485 );
1486 }
1487 self.rerender_widget_panel(panel_key);
1488 true
1489 }
1490
1491 fn handle_widget_tree_lateral(&mut self, panel_key: &crate::widgets::PanelKey, is_right: bool) {
1501 let panel = match self.widget_registry.get(panel_key) {
1502 Some(p) => p,
1503 None => return,
1504 };
1505 let focus_key = panel.focus_key.clone();
1506 if focus_key.is_empty() {
1507 return;
1508 }
1509 let widget = crate::widgets::find_widget_by_key(&panel.spec, &focus_key);
1510 let (spec_sel, nodes, item_keys) = match widget {
1511 Some(fresh_core::api::WidgetSpec::Tree {
1512 selected_index,
1513 nodes,
1514 item_keys,
1515 ..
1516 }) => (*selected_index, nodes.clone(), item_keys.clone()),
1517 _ => return,
1518 };
1519 if nodes.is_empty() {
1520 return;
1521 }
1522 let (cur_sel, cur_scroll, mut expanded) = match panel.instance_states.get(&focus_key) {
1523 Some(crate::widgets::WidgetInstanceState::Tree {
1524 selected_index,
1525 scroll_offset,
1526 expanded_keys,
1527 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
1528 _ => (spec_sel, 0u32, std::collections::HashSet::<String>::new()),
1529 };
1530 if cur_sel < 0 {
1531 return;
1532 }
1533 let sel_idx = cur_sel as usize;
1534 let node = match nodes.get(sel_idx) {
1535 Some(n) => n,
1536 None => return,
1537 };
1538 let key = item_keys.get(sel_idx).cloned().unwrap_or_default();
1539 let was_expanded = !key.is_empty() && expanded.contains(&key);
1540
1541 let mut new_sel = cur_sel;
1542 let mut expansion_changed: Option<bool> = None; if is_right {
1544 if node.has_children && !was_expanded && !key.is_empty() {
1545 expanded.insert(key.clone());
1546 expansion_changed = Some(true);
1547 }
1548 } else if node.has_children && was_expanded && !key.is_empty() {
1549 expanded.remove(&key);
1550 expansion_changed = Some(false);
1551 } else if let Some(parent_idx) = crate::widgets::tree_parent_index(&nodes, sel_idx) {
1552 new_sel = parent_idx as i32;
1553 }
1554 if expansion_changed.is_none() && new_sel == cur_sel {
1556 return;
1557 }
1558 let final_key = item_keys.get(new_sel as usize).cloned().unwrap_or_default();
1559 if let Some(panel_mut) = self.widget_registry.get_mut(panel_key) {
1560 panel_mut.instance_states.insert(
1561 focus_key.clone(),
1562 crate::widgets::WidgetInstanceState::Tree {
1563 scroll_offset: cur_scroll,
1564 selected_index: new_sel,
1565 expanded_keys: expanded,
1566 },
1567 );
1568 }
1569 self.rerender_widget_panel(panel_key);
1570 if let Some(now_expanded) = expansion_changed {
1571 self.fire_widget_event(
1572 panel_key,
1573 focus_key.clone(),
1574 "expand".into(),
1575 serde_json::json!({
1576 "index": cur_sel as i64,
1577 "key": key,
1578 "expanded": now_expanded,
1579 }),
1580 );
1581 } else if new_sel != cur_sel {
1582 self.fire_widget_event(
1583 panel_key,
1584 focus_key,
1585 "select".into(),
1586 serde_json::json!({
1587 "index": new_sel as i64,
1588 "key": final_key,
1589 }),
1590 );
1591 }
1592 }
1593
1594 pub(crate) fn handle_widget_tree_expand_toggle(
1598 &mut self,
1599 panel_key: &crate::widgets::PanelKey,
1600 widget_key: &str,
1601 item_key: &str,
1602 ) {
1603 if widget_key.is_empty() || item_key.is_empty() {
1604 return;
1605 }
1606 let now_expanded = {
1607 let panel = match self.widget_registry.get_mut(panel_key) {
1608 Some(p) => p,
1609 None => return,
1610 };
1611 let (cur_scroll, cur_sel, mut expanded) = match panel.instance_states.get(widget_key) {
1612 Some(crate::widgets::WidgetInstanceState::Tree {
1613 scroll_offset,
1614 selected_index,
1615 expanded_keys,
1616 }) => (*scroll_offset, *selected_index, expanded_keys.clone()),
1617 _ => (0u32, -1i32, std::collections::HashSet::<String>::new()),
1618 };
1619 let next = if expanded.contains(item_key) {
1620 expanded.remove(item_key);
1621 false
1622 } else {
1623 expanded.insert(item_key.to_string());
1624 true
1625 };
1626 panel.instance_states.insert(
1627 widget_key.to_string(),
1628 crate::widgets::WidgetInstanceState::Tree {
1629 scroll_offset: cur_scroll,
1630 selected_index: cur_sel,
1631 expanded_keys: expanded,
1632 },
1633 );
1634 next
1635 };
1636 self.rerender_widget_panel(panel_key);
1637 self.fire_widget_event(
1638 panel_key,
1639 widget_key.to_string(),
1640 "expand".into(),
1641 serde_json::json!({ "key": item_key, "expanded": now_expanded, }),
1642 );
1643 }
1644
1645 fn fire_tree_toggle_if_checkable(
1659 &mut self,
1660 panel_key: &crate::widgets::PanelKey,
1661 focus_key: &str,
1662 ) -> bool {
1663 let panel = match self.widget_registry.get(panel_key) {
1664 Some(p) => p,
1665 None => return false,
1666 };
1667 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
1668 let (spec_sel, nodes, item_keys, checkable) = match widget {
1669 Some(fresh_core::api::WidgetSpec::Tree {
1670 selected_index,
1671 nodes,
1672 item_keys,
1673 checkable,
1674 ..
1675 }) => (*selected_index, nodes, item_keys.clone(), *checkable),
1676 _ => return false,
1677 };
1678 if !checkable {
1679 return false;
1680 }
1681 let sel = match panel.instance_states.get(focus_key) {
1682 Some(crate::widgets::WidgetInstanceState::Tree { selected_index, .. }) => {
1683 *selected_index
1684 }
1685 _ => spec_sel,
1686 };
1687 if sel < 0 {
1688 return false;
1689 }
1690 let cur_checked = match nodes.get(sel as usize).and_then(|n| n.checked) {
1691 Some(b) => b,
1692 None => return false, };
1694 let new_checked = !cur_checked;
1695 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
1696 self.fire_widget_event(
1697 panel_key,
1698 focus_key.to_string(),
1699 "toggle".into(),
1700 serde_json::json!({ "index": sel, "key": item_key, "checked": new_checked, }),
1701 );
1702 true
1703 }
1704
1705 fn fire_tree_activate(&mut self, panel_key: &crate::widgets::PanelKey, focus_key: &str) {
1706 let panel = match self.widget_registry.get(panel_key) {
1707 Some(p) => p,
1708 None => return,
1709 };
1710 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
1711 let (spec_sel, item_keys) = match widget {
1712 Some(fresh_core::api::WidgetSpec::Tree {
1713 selected_index,
1714 item_keys,
1715 ..
1716 }) => (*selected_index, item_keys.clone()),
1717 _ => return,
1718 };
1719 let sel = match panel.instance_states.get(focus_key) {
1720 Some(crate::widgets::WidgetInstanceState::Tree { selected_index, .. }) => {
1721 *selected_index
1722 }
1723 _ => spec_sel,
1724 };
1725 if sel < 0 {
1726 return;
1727 }
1728 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
1729 self.fire_widget_event(
1730 panel_key,
1731 focus_key.to_string(),
1732 "activate".into(),
1733 serde_json::json!({ "index": sel, "key": item_key, }),
1734 );
1735 }
1736
1737 pub(super) fn focused_text_widget_panel_for_buffer(
1750 &self,
1751 buffer_id: crate::model::event::BufferId,
1752 ) -> Option<crate::widgets::PanelKey> {
1753 for panel_key in self.widget_registry.panels_for_buffer(buffer_id) {
1754 if self.panel_focused_widget_is_text(&panel_key) {
1755 return Some(panel_key);
1756 }
1757 }
1758 None
1759 }
1760
1761 pub(super) fn panel_focused_widget_is_text(
1769 &self,
1770 panel_key: &crate::widgets::PanelKey,
1771 ) -> bool {
1772 let Some(panel) = self.widget_registry.get(panel_key) else {
1773 return false;
1774 };
1775 if panel.focus_key.is_empty() {
1776 return false;
1777 }
1778 matches!(
1779 crate::widgets::find_widget_by_key(&panel.spec, &panel.focus_key),
1780 Some(fresh_core::api::WidgetSpec::Text { .. })
1781 )
1782 }
1783
1784 pub(super) fn focused_widget_selected_text(
1789 &self,
1790 panel_key: &crate::widgets::PanelKey,
1791 ) -> Option<String> {
1792 let panel = self.widget_registry.get(panel_key)?;
1793 if panel.focus_key.is_empty() {
1794 return None;
1795 }
1796 match panel.instance_states.get(&panel.focus_key) {
1797 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
1798 editor.selected_text()
1799 }
1800 _ => None,
1801 }
1802 }
1803
1804 pub(super) fn handle_widget_select_all(
1809 &mut self,
1810 panel_key: &crate::widgets::PanelKey,
1811 ) -> bool {
1812 self.with_focused_text_editor(panel_key, |editor| editor.select_all())
1816 }
1817
1818 pub(super) fn handle_widget_copy(&mut self, panel_key: &crate::widgets::PanelKey) -> bool {
1823 if self.widget_registry.get(panel_key).is_none() {
1824 return false;
1825 }
1826 if let Some(text) = self.focused_widget_selected_text(panel_key) {
1827 self.clipboard.copy(text);
1828 }
1829 true
1830 }
1831
1832 pub(super) fn handle_widget_cut(&mut self, panel_key: &crate::widgets::PanelKey) -> bool {
1835 if self.widget_registry.get(panel_key).is_none() {
1836 return false;
1837 }
1838 if let Some(text) = self.focused_widget_selected_text(panel_key) {
1839 self.clipboard.copy(text);
1840 self.with_focused_text_editor(panel_key, |editor| {
1841 editor.delete_selection();
1842 });
1843 }
1844 true
1845 }
1846
1847 pub(super) fn handle_widget_insert_str(
1853 &mut self,
1854 panel_key: &crate::widgets::PanelKey,
1855 text: &str,
1856 ) -> bool {
1857 if self.widget_registry.get(panel_key).is_none() {
1858 return false;
1859 }
1860 let owned = text.to_string();
1861 self.with_focused_text_editor(panel_key, move |editor| {
1862 editor.insert_str(&owned);
1863 });
1864 true
1865 }
1866
1867 fn ensure_focused_text_seeded(
1874 &mut self,
1875 panel_key: &crate::widgets::PanelKey,
1876 focus_key: &str,
1877 ) -> bool {
1878 let panel = match self.widget_registry.get_mut(panel_key) {
1879 Some(p) => p,
1880 None => return false,
1881 };
1882 if matches!(
1883 panel.instance_states.get(focus_key),
1884 Some(crate::widgets::WidgetInstanceState::Text { .. })
1885 ) {
1886 return true;
1887 }
1888 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
1889 let (value, cursor_byte, multiline) = match widget {
1890 Some(fresh_core::api::WidgetSpec::Text {
1891 value,
1892 cursor_byte,
1893 rows,
1894 ..
1895 }) => (value.clone(), *cursor_byte, *rows > 1),
1896 _ => return false,
1897 };
1898 let mut editor = if multiline {
1899 crate::primitives::text_edit::TextEdit::with_text(&value)
1900 } else {
1901 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
1902 };
1903 let seed = if cursor_byte < 0 {
1904 value.len()
1905 } else {
1906 (cursor_byte as usize).min(value.len())
1907 };
1908 editor.set_cursor_from_flat(seed);
1909 panel.instance_states.insert(
1910 focus_key.to_string(),
1911 crate::widgets::WidgetInstanceState::Text {
1912 editor,
1913 scroll: 0,
1914 completions: Vec::new(),
1915 completion_selected_index: 0,
1916 completion_scroll_offset: 0,
1917 completion_navigated: false,
1918 },
1919 );
1920 true
1921 }
1922
1923 pub(super) fn with_focused_text_editor<F>(
1930 &mut self,
1931 panel_key: &crate::widgets::PanelKey,
1932 op: F,
1933 ) -> bool
1934 where
1935 F: FnOnce(&mut crate::primitives::text_edit::TextEdit),
1936 {
1937 let focus_key = match self.widget_registry.get(panel_key) {
1938 Some(p) if !p.focus_key.is_empty() => p.focus_key.clone(),
1939 _ => return false,
1940 };
1941 if !self.ensure_focused_text_seeded(panel_key, &focus_key) {
1942 return false;
1943 }
1944 let (before_value, before_cursor) = {
1945 let panel = self.widget_registry.get(panel_key).unwrap();
1946 match panel.instance_states.get(&focus_key) {
1947 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
1948 (editor.value(), editor.flat_cursor_byte())
1949 }
1950 _ => return false,
1951 }
1952 };
1953 {
1954 let panel = self.widget_registry.get_mut(panel_key).unwrap();
1955 match panel.instance_states.get_mut(&focus_key) {
1956 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => op(editor),
1957 _ => return false,
1958 }
1959 }
1960 let (after_value, after_cursor) = {
1961 let panel = self.widget_registry.get(panel_key).unwrap();
1962 match panel.instance_states.get(&focus_key) {
1963 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
1964 (editor.value(), editor.flat_cursor_byte())
1965 }
1966 _ => return false,
1967 }
1968 };
1969 if after_value == before_value && after_cursor == before_cursor {
1970 return false;
1971 }
1972 self.rerender_widget_panel(panel_key);
1973 self.fire_widget_event(
1974 panel_key,
1975 focus_key.clone(),
1976 "change".into(),
1977 serde_json::json!({ "value": after_value, "cursorByte": after_cursor as i64, }),
1978 );
1979 true
1980 }
1981
1982 fn handle_widget_text_key(&mut self, panel_key: &crate::widgets::PanelKey, key: &str) {
1988 self.with_focused_text_editor(panel_key, |editor| match key {
1989 "Backspace" => editor.backspace(),
1990 "Delete" => editor.delete(),
1991 "Left" => editor.move_left(),
1992 "Right" => editor.move_right(),
1993 "Up" => editor.move_up(),
1994 "Down" => editor.move_down(),
1995 "Home" => editor.move_home(),
1996 "End" => editor.move_end(),
1997 "Enter" => editor.insert_char('\n'),
1998 _ => { }
1999 });
2000 }
2001
2002 fn handle_widget_text_char(&mut self, panel_key: &crate::widgets::PanelKey, text: &str) {
2009 if text.is_empty() {
2010 return;
2011 }
2012 let text = text.to_string();
2013 self.with_focused_text_editor(panel_key, move |editor| {
2014 editor.insert_str(&text);
2015 });
2016 }
2017
2018 pub(super) fn floating_panel_inner_width(&self, slot: super::PanelSlot) -> u32 {
2024 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
2027 self.panel(slot).map(|f| f.placement)
2028 {
2029 return (width_cols as u32).saturating_sub(2).max(10);
2030 }
2031 let term_w = self.terminal_width.max(1) as u32;
2032 let pct = self
2033 .panel(slot)
2034 .map(|f| f.width_pct.clamp(1, 100) as u32)
2035 .unwrap_or(80);
2036 let w = (term_w * pct) / 100;
2037 w.saturating_sub(2).max(10)
2038 }
2039
2040 pub(super) fn refocus_floating_panel(&mut self, slot: super::PanelSlot) {
2058 let Some(panel_key) = self.panel(slot).map(|f| f.panel_key.clone()) else {
2059 return;
2060 };
2061 if let Some(f) = self.panel_mut(slot) {
2062 f.focused = true;
2063 }
2064 let widget_key = self
2065 .widget_registry
2066 .get(&panel_key)
2067 .map(|p| p.focus_key.clone())
2068 .unwrap_or_default();
2069 tracing::debug!(
2070 target: "fresh::dock",
2071 panel = %panel_key,
2072 ?slot,
2073 widget_key = %widget_key,
2074 "refocus_floating_panel: firing unconditional `focus` widget_event"
2075 );
2076 self.fire_widget_event(
2077 &panel_key,
2078 widget_key,
2079 "focus".to_string(),
2080 serde_json::json!({ "previous": "(re-focus)" }),
2081 );
2082 }
2083
2084 pub(super) fn blur_floating_panel(&mut self, slot: super::PanelSlot) {
2091 let Some(panel_key) = self.panel(slot).map(|f| f.panel_key.clone()) else {
2092 return;
2093 };
2094 if let Some(f) = self.panel_mut(slot) {
2095 f.focused = false;
2096 }
2097 tracing::debug!(
2098 target: "fresh::dock",
2099 panel = %panel_key,
2100 ?slot,
2101 "blur_floating_panel: firing `blur` widget_event"
2102 );
2103 let widget_key = self
2104 .widget_registry
2105 .get(&panel_key)
2106 .map(|p| p.focus_key.clone())
2107 .unwrap_or_default();
2108 self.fire_widget_event(
2109 &panel_key,
2110 widget_key,
2111 "blur".to_string(),
2112 serde_json::json!({}),
2113 );
2114 }
2115
2116 pub(super) fn handle_close_split(&mut self, split_id: SplitId) {
2118 let leaf_id = LeafId(split_id);
2120 match self
2121 .windows
2122 .get_mut(&self.active_window)
2123 .and_then(|w| w.split_manager_mut())
2124 .expect("active window must have a populated split layout")
2125 .close_split(leaf_id)
2126 {
2127 Ok(()) => {
2128 self.windows
2130 .get_mut(&self.active_window)
2131 .and_then(|w| w.split_view_states_mut())
2132 .expect("active window must have a populated split layout")
2133 .remove(&leaf_id);
2134 tracing::info!("Closed split {:?}", split_id);
2135 }
2136 Err(e) => {
2137 tracing::warn!("Failed to close split {:?}: {}", split_id, e);
2138 }
2139 }
2140 }
2141
2142 pub(super) fn handle_refresh_lines(&mut self, buffer_id: BufferId) {
2144 self.active_window_mut().seen_byte_ranges.remove(&buffer_id);
2148 #[cfg(feature = "plugins")]
2150 {
2151 self.plugin_render_requested = true;
2152 }
2153 }
2154
2155 pub(super) fn flush_pending_grammars(&mut self) {
2166 if self.needs_full_grammar_build {
2170 self.needs_full_grammar_build = false;
2171 self.grammar_reload_pending = false;
2172
2173 let additional: Vec<_> = self
2175 .pending_grammars
2176 .drain(..)
2177 .map(|g| crate::primitives::grammar::GrammarSpec {
2178 language: g.language.clone(),
2179 path: std::path::PathBuf::from(g.grammar_path),
2180 extensions: g.extensions.clone(),
2181 })
2182 .collect();
2183
2184 for crate::primitives::grammar::GrammarSpec {
2186 language,
2187 extensions,
2188 ..
2189 } in &additional
2190 {
2191 let lang_config = self
2192 .config_mut()
2193 .languages
2194 .entry(language.clone())
2195 .or_default();
2196 for ext in extensions {
2197 if !lang_config.extensions.contains(ext) {
2198 lang_config.extensions.push(ext.clone());
2199 }
2200 }
2201 }
2202
2203 let callback_ids: Vec<_> = self.pending_grammar_callbacks.drain(..).collect();
2204 self.start_background_grammar_build(additional, callback_ids);
2205 return;
2206 }
2207
2208 if !self.grammar_reload_pending {
2209 return;
2210 }
2211 self.grammar_reload_pending = false;
2212
2213 if self.grammar_build_in_progress {
2217 self.grammar_reload_pending = true;
2218 tracing::debug!("Grammar build in progress, deferring flush");
2219 return;
2220 }
2221
2222 use std::path::PathBuf;
2223
2224 if self.pending_grammars.is_empty() {
2225 tracing::debug!("Grammar reload requested but no pending grammars");
2226 return;
2227 }
2228
2229 let pending_before = self.pending_grammars.len();
2233 self.pending_grammars.retain(|g| {
2234 let all_mapped = !g.extensions.is_empty()
2236 && g.extensions
2237 .iter()
2238 .all(|ext| self.grammar_registry.find_by_extension(ext).is_some());
2239 if all_mapped {
2240 tracing::debug!(
2241 "Skipping already-loaded grammar '{}' (extensions {:?} already mapped)",
2242 g.language,
2243 g.extensions
2244 );
2245 false
2246 } else {
2247 true
2248 }
2249 });
2250 if pending_before != self.pending_grammars.len() {
2251 tracing::info!(
2252 "Deduplicated pending grammars: {} -> {}",
2253 pending_before,
2254 self.pending_grammars.len()
2255 );
2256 }
2257
2258 if self.pending_grammars.is_empty() {
2259 tracing::info!(
2260 "All pending grammars already loaded, resolving callbacks without rebuild"
2261 );
2262 #[cfg(feature = "plugins")]
2264 for cb_id in self.pending_grammar_callbacks.drain(..) {
2265 self.plugin_manager
2266 .read()
2267 .unwrap()
2268 .resolve_callback(cb_id, "null".to_string());
2269 }
2270 #[cfg(not(feature = "plugins"))]
2271 self.pending_grammar_callbacks.clear();
2272 return;
2273 }
2274
2275 tracing::info!(
2276 "Flushing {} pending grammars via background rebuild",
2277 self.pending_grammars.len()
2278 );
2279
2280 let additional: Vec<crate::primitives::grammar::GrammarSpec> = self
2282 .pending_grammars
2283 .drain(..)
2284 .map(|g| crate::primitives::grammar::GrammarSpec {
2285 language: g.language.clone(),
2286 path: PathBuf::from(g.grammar_path),
2287 extensions: g.extensions.clone(),
2288 })
2289 .collect();
2290
2291 for crate::primitives::grammar::GrammarSpec {
2293 language,
2294 extensions,
2295 ..
2296 } in &additional
2297 {
2298 let lang_config = self
2299 .config_mut()
2300 .languages
2301 .entry(language.clone())
2302 .or_default();
2303 for ext in extensions {
2304 if !lang_config.extensions.contains(ext) {
2305 lang_config.extensions.push(ext.clone());
2306 }
2307 }
2308 }
2309
2310 let callback_ids: Vec<_> = self.pending_grammar_callbacks.drain(..).collect();
2312
2313 let base_registry = std::sync::Arc::clone(&self.grammar_registry);
2315 if let Some(bridge) = &self.async_bridge {
2316 let sender = bridge.sender();
2317 self.grammar_build_in_progress = true;
2318 std::thread::Builder::new()
2319 .name("grammar-rebuild".to_string())
2320 .spawn(move || {
2321 use crate::primitives::grammar::GrammarRegistry;
2322 match GrammarRegistry::with_additional_grammars(&base_registry, &additional) {
2323 Some(new_registry) => {
2324 drop(sender.send(
2326 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
2327 registry: std::sync::Arc::new(new_registry),
2328 callback_ids,
2329 },
2330 ));
2331 }
2332 None => {
2333 tracing::error!("Failed to rebuild grammar registry in background");
2334 drop(sender.send(
2336 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
2337 registry: base_registry,
2338 callback_ids,
2339 },
2340 ));
2341 }
2342 }
2343 })
2344 .ok();
2345 }
2346 }
2347
2348 pub(crate) fn drain_pending_vb_animations(&mut self) {
2355 if self.pending_vb_animations.is_empty() {
2356 return;
2357 }
2358 let pending = std::mem::take(&mut self.pending_vb_animations);
2359 for (id, buffer_id, kind) in pending {
2360 match self.virtual_buffer_screen_rect(buffer_id) {
2361 Some(area) => {
2362 let animation_kind = translate_plugin_animation_kind(kind);
2363 self.active_window_mut().animations.start_with_id(
2364 crate::view::animation::AnimationId::from_raw(id),
2365 area,
2366 animation_kind,
2367 );
2368 }
2369 None => {
2370 self.pending_vb_animations.push((id, buffer_id, kind));
2372 }
2373 }
2374 }
2375 }
2376
2377 pub(crate) fn virtual_buffer_screen_rect(
2380 &self,
2381 buffer_id: BufferId,
2382 ) -> Option<ratatui::layout::Rect> {
2383 self.active_layout()
2384 .split_areas
2385 .iter()
2386 .find(|(_, bid, _, _, _, _)| *bid == buffer_id)
2387 .map(|(_, _, content_rect, _, _, _)| *content_rect)
2388 }
2389}