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