Skip to main content

fresh/app/
view_actions.rs

1//! View mode action handlers.
2//!
3//! This module contains handlers for view-related actions like compose mode
4//! toggling. All bodies live on `impl Window` — none of the helpers reach
5//! editor-global state (plugin manager, mode registry, etc.); they manipulate
6//! per-window split-view-state and animations.
7
8use crate::app::window::Window;
9use crate::model::event::LeafId;
10use crate::state::ViewMode;
11use rust_i18n::t;
12
13impl Window {
14    /// Toggle between Compose and Source view modes for the active split.
15    pub fn handle_toggle_page_view(&mut self) {
16        let (mgr, _) = self
17            .buffers
18            .splits()
19            .expect("active window must have a populated split layout");
20        let active_split = mgr.active_split();
21        let active_buffer = mgr
22            .get_buffer_id(active_split.into())
23            .unwrap_or(crate::model::event::BufferId(0));
24        let default_wrap = self.resolve_line_wrap_for_buffer(active_buffer);
25        let default_line_numbers = self.config().editor.line_numbers;
26        let page_width = self
27            .buffers
28            .get(&active_buffer)
29            .and_then(|s| self.config().languages.get(&s.language))
30            .and_then(|lc| lc.page_width)
31            .or(self.config().editor.page_width);
32
33        let view_mode = {
34            let (_, vs_map) = self
35                .buffers
36                .splits()
37                .expect("active window must have a populated split layout");
38            let current = vs_map
39                .get(&active_split)
40                .map(|vs| vs.view_mode.clone())
41                .unwrap_or(ViewMode::Source);
42            match current {
43                ViewMode::PageView => ViewMode::Source,
44                _ => ViewMode::PageView,
45            }
46        };
47
48        // Update split view state (source of truth for view mode and line numbers)
49        if let Some(vs) = self
50            .split_view_states_mut()
51            .expect("active window must have a populated split layout")
52            .get_mut(&active_split)
53        {
54            vs.view_mode = view_mode.clone();
55            // In Compose mode, disable builtin line wrap - the plugin handles
56            // wrapping by inserting Break tokens in the view transform pipeline.
57            // In Source mode, respect the user's default_wrap preference.
58            vs.viewport.line_wrap_enabled = match view_mode {
59                ViewMode::PageView => false,
60                // A per-buffer override wins over the global/language default
61                // when returning to Source mode.
62                ViewMode::Source => vs.line_wrap_override.unwrap_or(default_wrap),
63            };
64            match view_mode {
65                ViewMode::PageView => {
66                    vs.show_line_numbers = false;
67                    // Apply page_width from language config if available
68                    if let Some(width) = page_width {
69                        vs.compose_width = Some(width as u16);
70                    }
71                }
72                ViewMode::Source => {
73                    // Clear compose width to remove margins
74                    vs.compose_width = None;
75                    vs.view_transform = None;
76                    // A per-buffer override wins over the global default.
77                    vs.show_line_numbers = vs.line_numbers_override.unwrap_or(default_line_numbers);
78                }
79            }
80        }
81
82        let mode_label = match view_mode {
83            ViewMode::PageView => t!("view.page_view").to_string(),
84            ViewMode::Source => "Source".to_string(),
85        };
86        self.set_status_message(t!("view.mode", mode = mode_label).to_string());
87    }
88
89    /// Start a horizontal slide over the given split's content area to
90    /// visualize a tab switch. `direction`: +1 = the new tab is to
91    /// the right of the previous one in tab order, so the new view
92    /// pushes in from the right; -1 = the new tab is to the left,
93    /// view pushes in from the left; 0 = no animation.
94    ///
95    /// The split's Rect is resolved from the cached layout captured
96    /// in the last render pass. If the split isn't on screen yet
97    /// (freshly created) the call is a no-op — animation is a purely
98    /// decorative layer and missing it does not affect correctness.
99    pub(crate) fn animate_tab_switch(&mut self, split_id: LeafId, direction: i32) {
100        if direction == 0 {
101            return;
102        }
103        if !self.config().editor.animations {
104            return;
105        }
106        let Some(area) = self.split_or_group_content_rect(split_id) else {
107            return;
108        };
109        if area.width == 0 || area.height == 0 {
110            return;
111        }
112        let from = if direction > 0 {
113            crate::view::animation::Edge::Right
114        } else {
115            crate::view::animation::Edge::Left
116        };
117        self.animations.start(
118            area,
119            crate::view::animation::AnimationKind::SlideIn {
120                from,
121                duration: std::time::Duration::from_millis(260),
122                delay: std::time::Duration::ZERO,
123            },
124        );
125    }
126
127    /// Resolve the on-screen Rect that covers the split `split_id` from
128    /// the cached layout.
129    ///
130    /// Normally a split_id maps 1:1 to a single entry in
131    /// `WindowLayoutCache::split_areas` (the split's content rect). When a
132    /// buffer-group tab is active, however, the split renders the
133    /// group's inner subtree — split_areas then has one entry per
134    /// inner panel (log / detail / toolbar etc.) and NO entry for the
135    /// outer split id. In that case we walk the stashed group subtree
136    /// to collect every inner LeafId, look each one up in split_areas,
137    /// and return the bounding box. That gives us the overall area the
138    /// group occupies on screen.
139    fn split_or_group_content_rect(&self, split_id: LeafId) -> Option<ratatui::layout::Rect> {
140        if let Some(rect) = self
141            .layout_cache
142            .split_areas
143            .iter()
144            .find(|(sid, _, _, _, _, _)| *sid == split_id)
145            .map(|(_, _, content_rect, _, _, _)| *content_rect)
146        {
147            return Some(rect);
148        }
149
150        // Fallback: is this split hosting a buffer-group tab? If so,
151        // walk the group's inner subtree to collect its leaf ids and
152        // union their cached content rects.
153        let (_, vs_map) = self
154            .buffers
155            .splits()
156            .expect("active window must have a populated split layout");
157        let group_leaf = vs_map.get(&split_id).and_then(|vs| vs.active_group_tab)?;
158        let subtree = self.grouped_subtrees.get(&group_leaf)?;
159
160        let mut inner_leaves: Vec<LeafId> = Vec::new();
161        collect_leaf_ids(subtree, &mut inner_leaves);
162
163        let mut union: Option<ratatui::layout::Rect> = None;
164        for (sid, _, content, _, _, _) in &self.layout_cache.split_areas {
165            if !inner_leaves.contains(sid) {
166                continue;
167            }
168            union = Some(match union {
169                None => *content,
170                Some(prev) => rect_union(prev, *content),
171            });
172        }
173        union
174    }
175}
176
177/// Walk a SplitNode collecting every Leaf's `split_id`.
178fn collect_leaf_ids(node: &crate::view::split::SplitNode, out: &mut Vec<LeafId>) {
179    use crate::view::split::SplitNode;
180    match node {
181        SplitNode::Leaf { split_id, .. } => out.push(*split_id),
182        SplitNode::Split { first, second, .. } => {
183            collect_leaf_ids(first, out);
184            collect_leaf_ids(second, out);
185        }
186        SplitNode::Grouped { layout, .. } => collect_leaf_ids(layout, out),
187    }
188}
189
190fn rect_union(a: ratatui::layout::Rect, b: ratatui::layout::Rect) -> ratatui::layout::Rect {
191    let x = a.x.min(b.x);
192    let y = a.y.min(b.y);
193    let right = a.x.saturating_add(a.width).max(b.x.saturating_add(b.width));
194    let bottom =
195        a.y.saturating_add(a.height)
196            .max(b.y.saturating_add(b.height));
197    ratatui::layout::Rect::new(x, y, right.saturating_sub(x), bottom.saturating_sub(y))
198}