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                ViewMode::Source => default_wrap,
61            };
62            match view_mode {
63                ViewMode::PageView => {
64                    vs.show_line_numbers = false;
65                    // Apply page_width from language config if available
66                    if let Some(width) = page_width {
67                        vs.compose_width = Some(width as u16);
68                    }
69                }
70                ViewMode::Source => {
71                    // Clear compose width to remove margins
72                    vs.compose_width = None;
73                    vs.view_transform = None;
74                    vs.show_line_numbers = default_line_numbers;
75                }
76            }
77        }
78
79        let mode_label = match view_mode {
80            ViewMode::PageView => t!("view.page_view").to_string(),
81            ViewMode::Source => "Source".to_string(),
82        };
83        self.set_status_message(t!("view.mode", mode = mode_label).to_string());
84    }
85
86    /// Start a horizontal slide over the given split's content area to
87    /// visualize a tab switch. `direction`: +1 = the new tab is to
88    /// the right of the previous one in tab order, so the new view
89    /// pushes in from the right; -1 = the new tab is to the left,
90    /// view pushes in from the left; 0 = no animation.
91    ///
92    /// The split's Rect is resolved from the cached layout captured
93    /// in the last render pass. If the split isn't on screen yet
94    /// (freshly created) the call is a no-op — animation is a purely
95    /// decorative layer and missing it does not affect correctness.
96    pub(crate) fn animate_tab_switch(&mut self, split_id: LeafId, direction: i32) {
97        if direction == 0 {
98            return;
99        }
100        if !self.config().editor.animations {
101            return;
102        }
103        let Some(area) = self.split_or_group_content_rect(split_id) else {
104            return;
105        };
106        if area.width == 0 || area.height == 0 {
107            return;
108        }
109        let from = if direction > 0 {
110            crate::view::animation::Edge::Right
111        } else {
112            crate::view::animation::Edge::Left
113        };
114        self.animations.start(
115            area,
116            crate::view::animation::AnimationKind::SlideIn {
117                from,
118                duration: std::time::Duration::from_millis(260),
119                delay: std::time::Duration::ZERO,
120            },
121        );
122    }
123
124    /// Resolve the on-screen Rect that covers the split `split_id` from
125    /// the cached layout.
126    ///
127    /// Normally a split_id maps 1:1 to a single entry in
128    /// `WindowLayoutCache::split_areas` (the split's content rect). When a
129    /// buffer-group tab is active, however, the split renders the
130    /// group's inner subtree — split_areas then has one entry per
131    /// inner panel (log / detail / toolbar etc.) and NO entry for the
132    /// outer split id. In that case we walk the stashed group subtree
133    /// to collect every inner LeafId, look each one up in split_areas,
134    /// and return the bounding box. That gives us the overall area the
135    /// group occupies on screen.
136    fn split_or_group_content_rect(&self, split_id: LeafId) -> Option<ratatui::layout::Rect> {
137        if let Some(rect) = self
138            .layout_cache
139            .split_areas
140            .iter()
141            .find(|(sid, _, _, _, _, _)| *sid == split_id)
142            .map(|(_, _, content_rect, _, _, _)| *content_rect)
143        {
144            return Some(rect);
145        }
146
147        // Fallback: is this split hosting a buffer-group tab? If so,
148        // walk the group's inner subtree to collect its leaf ids and
149        // union their cached content rects.
150        let (_, vs_map) = self
151            .buffers
152            .splits()
153            .expect("active window must have a populated split layout");
154        let group_leaf = vs_map.get(&split_id).and_then(|vs| vs.active_group_tab)?;
155        let subtree = self.grouped_subtrees.get(&group_leaf)?;
156
157        let mut inner_leaves: Vec<LeafId> = Vec::new();
158        collect_leaf_ids(subtree, &mut inner_leaves);
159
160        let mut union: Option<ratatui::layout::Rect> = None;
161        for (sid, _, content, _, _, _) in &self.layout_cache.split_areas {
162            if !inner_leaves.contains(sid) {
163                continue;
164            }
165            union = Some(match union {
166                None => *content,
167                Some(prev) => rect_union(prev, *content),
168            });
169        }
170        union
171    }
172}
173
174/// Walk a SplitNode collecting every Leaf's `split_id`.
175fn collect_leaf_ids(node: &crate::view::split::SplitNode, out: &mut Vec<LeafId>) {
176    use crate::view::split::SplitNode;
177    match node {
178        SplitNode::Leaf { split_id, .. } => out.push(*split_id),
179        SplitNode::Split { first, second, .. } => {
180            collect_leaf_ids(first, out);
181            collect_leaf_ids(second, out);
182        }
183        SplitNode::Grouped { layout, .. } => collect_leaf_ids(layout, out),
184    }
185}
186
187fn rect_union(a: ratatui::layout::Rect, b: ratatui::layout::Rect) -> ratatui::layout::Rect {
188    let x = a.x.min(b.x);
189    let y = a.y.min(b.y);
190    let right = a.x.saturating_add(a.width).max(b.x.saturating_add(b.width));
191    let bottom =
192        a.y.saturating_add(a.height)
193            .max(b.y.saturating_add(b.height));
194    ratatui::layout::Rect::new(x, y, right.saturating_sub(x), bottom.saturating_sub(y))
195}