aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/mobile/src/screens/viewport.rs
blob: 96c78554cafd5122c8dee12dcd3013949d0cee72 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
use zellij_tile::prelude::*;

use crate::ansi::{move_to, slice_ansi_visible, visible_width, RESET};
use crate::click::ViewportRegion;
use crate::frame::Frame;
use crate::input::Input;
use crate::keys;
use crate::workspace::{pane_id_of, Workspace};

const DISABLE_AUTOWRAP: &str = "\x1b[?7l";
const ENABLE_AUTOWRAP: &str = "\x1b[?7h";

#[derive(Default)]
pub struct ViewportScreen {
    pub viewport_v_pan: usize,
    pub viewport_h_pan: usize,
    pub viewport_region: Option<ViewportRegion>,
}

impl ViewportScreen {
    pub fn reset_pan(&mut self) {
        self.viewport_v_pan = 0;
        self.viewport_h_pan = 0;
    }

    pub fn handle_key(&self, ws: &Workspace, input: &mut Input, key: KeyWithModifier) -> bool {
        if let Some(pane) = ws.current_pane() {
            let key = if input.ctrl_held || input.alt_held {
                input.merge_held_modifiers(&key)
            } else {
                key
            };
            let bytes = keys::serialize_key(&key);
            if !bytes.is_empty() {
                write_to_pane_id(bytes, pane_id_of(&pane));
            }
        }
        let consumed = input.ctrl_held || input.alt_held;
        input.ctrl_held = false;
        input.alt_held = false;
        consumed
    }

    pub fn max_viewport_v_pan(&self, ws: &Workspace) -> Option<usize> {
        let region = self.viewport_region?;
        let embed_height = region.row_end.saturating_sub(region.row_start);
        Some(ws.current_pane_viewport_len().saturating_sub(embed_height))
    }

    pub fn click_in_viewport(&self, row: usize, col: usize) -> Option<(usize, usize)> {
        let region = self.viewport_region?;
        if row < region.row_start || row >= region.row_end {
            return None;
        }
        if col >= region.cols {
            return None;
        }
        let pane_row = region.skip + (row - region.row_start);
        let pane_col = region.h_offset + col;
        Some((pane_row, pane_col))
    }

    pub fn compute_cursor_position(
        &self,
        ws: &Workspace,
        viewport_top: usize,
        viewport_height: usize,
        cols: usize,
        skip: usize,
        h_offset: usize,
    ) -> Option<(usize, usize)> {
        if viewport_height == 0 {
            return None;
        }
        let pane = ws.current_pane()?;
        let pane_id = pane_id_of(&pane);
        let (cursor_x, cursor_y) = ws.latest_pane_contents.get(&pane_id)?.cursor?;
        if cursor_y < skip {
            return None;
        }
        let row_in_slice = cursor_y - skip;
        if row_in_slice >= viewport_height {
            return None;
        }
        if cursor_x < h_offset {
            return None;
        }
        let plugin_x = cursor_x - h_offset;
        if plugin_x >= cols {
            return None;
        }
        let plugin_y = viewport_top + row_in_slice;
        Some((plugin_x, plugin_y))
    }

    pub fn render(
        &mut self,
        ws: &Workspace,
        _frame: &mut Frame,
        row_start: usize,
        row_end: usize,
        cols: usize,
    ) {
        let height = row_end - row_start;
        if height == 0 {
            return;
        }

        let pane = ws.current_pane();
        let pane_id = pane.as_ref().map(pane_id_of);
        let viewport_lines: Vec<String> = pane_id
            .and_then(|id| ws.latest_pane_contents.get(&id))
            .map(|c| c.viewport.clone())
            .unwrap_or_default();

        let max_v_pan = self
            .max_viewport_v_pan(ws)
            .unwrap_or_else(|| viewport_lines.len().saturating_sub(height));
        self.viewport_v_pan = self.viewport_v_pan.min(max_v_pan);
        let skip = max_v_pan.saturating_sub(self.viewport_v_pan);
        let pane_width = pane
            .as_ref()
            .map(|p| p.pane_content_columns)
            .filter(|&w| w > 0)
            .unwrap_or_else(|| {
                viewport_lines
                    .iter()
                    .map(|l| visible_width(l))
                    .max()
                    .unwrap_or(0)
            });
        let max_h_pan = pane_width.saturating_sub(cols);
        self.viewport_h_pan = self.viewport_h_pan.min(max_h_pan);
        let h_offset = self.viewport_h_pan;

        self.viewport_region = Some(ViewportRegion {
            row_start,
            row_end,
            cols,
            skip,
            h_offset,
        });

        print!("{}", DISABLE_AUTOWRAP);

        for i in 0..height {
            let row = row_start + i;
            print!("{}{}", RESET, move_to(row, 0));
            if let Some(line) = viewport_lines.get(skip + i) {
                if h_offset == 0 {
                    print!("{}", line);
                } else {
                    let sliced = slice_ansi_visible(line, h_offset, cols);
                    print!("{}", sliced);
                }
            } else if i == 0 && pane_id.is_none() {
                print!("{}(no pane selected)", RESET);
            } else if i == 0 && viewport_lines.is_empty() {
                print!("{}(awaiting first render…)", RESET);
            }
            let raw_width = viewport_lines
                .get(skip + i)
                .map(|l| visible_width(l))
                .unwrap_or(0);
            let printed_width = raw_width.saturating_sub(h_offset).min(cols);
            if printed_width < cols {
                print!("{}\x1b[K", RESET);
            }
        }

        print!("{}", ENABLE_AUTOWRAP);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::click::ViewportRegion;

    fn state_with_viewport(
        viewport_len: usize,
        embed_height: Option<usize>,
    ) -> (Workspace, ViewportScreen) {
        let mut ws = Workspace::default();
        let mut tab = TabInfo::default();
        tab.position = 0;
        ws.tabs.push(tab);
        ws.selected_tab_position = Some(0);

        let mut pane = PaneInfo::default();
        pane.id = 42;
        pane.is_plugin = false;
        pane.is_selectable = true;
        pane.is_suppressed = false;
        ws.panes_by_tab_position.insert(0, vec![pane]);
        ws.selected_pane_id = Some(PaneId::Terminal(42));

        let mut contents = PaneContents::default();
        contents.viewport = vec![String::new(); viewport_len];
        ws.latest_pane_contents
            .insert(PaneId::Terminal(42), contents);

        let mut vp = ViewportScreen::default();
        if let Some(h) = embed_height {
            vp.viewport_region = Some(ViewportRegion {
                row_start: 0,
                row_end: h,
                cols: 80,
                skip: 0,
                h_offset: 0,
            });
        }
        (ws, vp)
    }

    #[test]
    fn max_viewport_v_pan_none_without_region() {
        let (ws, vp) = state_with_viewport(100, None);
        assert_eq!(vp.max_viewport_v_pan(&ws), None);
    }

    #[test]
    fn max_viewport_v_pan_some_typical() {
        let (ws, vp) = state_with_viewport(100, Some(20));
        assert_eq!(vp.max_viewport_v_pan(&ws), Some(80));
    }

    #[test]
    fn max_viewport_v_pan_saturates_when_embed_larger() {
        let (ws, vp) = state_with_viewport(10, Some(20));
        assert_eq!(vp.max_viewport_v_pan(&ws), Some(0));
    }

    #[test]
    fn max_viewport_v_pan_empty_cache() {
        let (ws, vp) = state_with_viewport(0, Some(20));
        assert_eq!(vp.max_viewport_v_pan(&ws), Some(0));
    }
}