brewery
notashelf /
5cba919c783e11bc77983157fe8b9003fd6d6b67

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/wayland/rendering.rsRust227 lines8.5 KB
1
use super::*;
2
3
/// What determines one rendered row's pixels: its cells, the cursor on it, the
4
/// selection span over it, and the blink phase. Two equal `RowSnap`s render
5
/// identically, so a buffer holding an equal snapshot needs no repaint.
6
#[derive(Clone, PartialEq, Debug)]
7
struct RowSnap {
8
    cells: Vec<Cell>,
9
    /// `(col, shape, focused)` when the cursor is drawn on this row.
10
    cursor: Option<(usize, CursorShape, bool)>,
11
    /// Inclusive selected column span on this row.
12
    sel: Option<(usize, usize)>,
13
    /// Search-match spans `(lo, hi, is_current)` highlighted on this row.
14
    search: Vec<(usize, usize, bool)>,
15
    /// Search-prompt text drawn over this row (only the bottom row, when active).
16
    overlay: Option<String>,
17
    /// IME preedit `(start_col, text)` drawn inline over this row (cursor row).
18
    preedit: Option<(usize, String)>,
19
    /// Blink phase, but only varied when the row actually has blinking ink, so
20
    /// non-blinking rows stay equal across phase toggles.
21
    blink: bool,
22
}
23
24
/// One shm buffer plus the per-row snapshot of what it currently displays.
25
#[derive(Debug)]
26
pub(super) struct FrameBuf {
27
    buffer: Buffer,
28
    rows: Vec<RowSnap>,
29
}
30
31
/// Snapshot the determinants of viewport row `y`'s pixels.
32
fn row_snap(grid: &Grid, y: usize, focused: bool, blink_on: bool) -> RowSnap {
33
    let abs = grid.view_to_abs(y);
34
    let cells = grid.view_row(y).to_vec();
35
    let cursor = if grid.view_at_bottom() && grid.cursor().1 == y {
36
        let visible = grid.cursor_visible() && (!grid.cursor_blink() || blink_on);
37
        visible.then(|| (grid.cursor().0, grid.cursor_shape(), focused))
38
    } else {
39
        None
40
    };
41
    let has_blink = cells
42
        .iter()
43
        .any(|c| c.flags.contains(crate::grid::Flags::BLINK));
44
    RowSnap {
45
        cells,
46
        cursor,
47
        sel: grid.selection_span_on(abs),
48
        search: grid.search_spans_on(abs),
49
        overlay: None,
50
        preedit: None,
51
        blink: if has_blink { blink_on } else { true },
52
    }
53
}
54
impl App {
55
    /// Render only the rows that changed since the chosen buffer last displayed
56
    /// them, damage just those rows, and commit with a frame-callback request.
57
    pub(super) fn present(&mut self) {
58
        self.needs_draw = false;
59
        // URL hint labels overlay the grid but are not part of the row snapshot,
60
        // so force a full redraw while the labels are showing.
61
        if self.url_mode {
62
            self.frames.clear();
63
        }
64
        // Render into a buffer sized in physical pixels (logical × scale); the
65
        // viewport presents it back at the logical surface size.
66
        let (w, h) = self.phys_dims();
67
        let m = self.renderer.metrics();
68
        let (focused, blink_on) = (self.focused, self.blink_on);
69
70
        // A resize invalidates every buffer's contents and size.
71
        if self.buf_dims != (w, h) {
72
            self.frames.clear();
73
            self.buf_dims = (w, h);
74
        }
75
76
        let Some(session) = self.session.as_ref() else {
77
            return;
78
        };
79
        let grid = session.term.grid();
80
        // The visual bell inverts fg/bg for the duration of the flash.
81
        let flashed = self.flashing.then(|| session.term.theme().inverted());
82
        let theme = flashed.as_ref().unwrap_or(session.term.theme());
83
        let rows = grid.rows();
84
        let mut cur: Vec<RowSnap> = (0..rows)
85
            .map(|y| row_snap(grid, y, focused, blink_on))
86
            .collect();
87
88
        // The search prompt occupies the bottom row while search mode is active.
89
        // Recording it in the snapshot keeps the row's damage/diff correct.
90
        let bar_text = self.searching.then(|| {
91
            let (n, total) = grid.search_count();
92
            format!(
93
                "search: {}  [{n}/{total}]",
94
                grid.search_query().unwrap_or("")
95
            )
96
        });
97
        if let Some(text) = &bar_text
98
            && rows > 0
99
        {
100
            cur[rows - 1].overlay = Some(text.clone());
101
        }
102
103
        // The IME preedit is drawn inline at the cursor while composing.
104
        if !self.preedit.is_empty() && grid.view_at_bottom() {
105
            let (cx, cy) = grid.cursor();
106
            if cy < rows {
107
                cur[cy].preedit = Some((cx, self.preedit.clone()));
108
            }
109
        }
110
111
        // Reuse a buffer the compositor has released, else grow the ring.
112
        let stride = w as i32 * 4;
113
        let mut idx = None;
114
        for i in 0..self.frames.len() {
115
            if self.pool.canvas(&self.frames[i].buffer).is_some() {
116
                idx = Some(i);
117
                break;
118
            }
119
        }
120
        let idx = match idx {
121
            Some(i) => i,
122
            None if self.frames.len() < MAX_BUFFERS => {
123
                match self
124
                    .pool
125
                    .create_buffer(w as i32, h as i32, stride, wl_shm::Format::Argb8888)
126
                {
127
                    Ok((buffer, _)) => {
128
                        self.frames.push(FrameBuf {
129
                            buffer,
130
                            rows: Vec::new(),
131
                        });
132
                        self.frames.len() - 1
133
                    }
134
                    Err(err) => {
135
                        tracing::error!("allocate shm buffer: {err}");
136
                        return;
137
                    }
138
                }
139
            }
140
            // All buffers are still held by the compositor; a release event will
141
            // wake us and `needs_draw` (re-set below) retries then.
142
            None => {
143
                self.needs_draw = true;
144
                return;
145
            }
146
        };
147
148
        // Rows that differ from what this buffer last showed (all, if fresh).
149
        let prev = &self.frames[idx].rows;
150
        let dirty: Vec<usize> = (0..rows)
151
            .filter(|&y| prev.get(y) != Some(&cur[y]))
152
            .collect();
153
        if dirty.is_empty() {
154
            return;
155
        }
156
157
        // A buffer used for the first time has uninitialized margins; paint the
158
        // whole thing (background + padding) once, then damage it in full below.
159
        let fresh = self.frames[idx].rows.is_empty();
160
        let pad_y = self.to_phys(self.config.main.pad_y) as i32;
161
        let Some(canvas) = self.pool.canvas(&self.frames[idx].buffer) else {
162
            return;
163
        };
164
        let dims = (w as usize, h as usize);
165
        let frame = crate::render::Frame {
166
            theme,
167
            focused,
168
            blink_on,
169
            hovered_link: self.hovered_link,
170
        };
171
        if fresh {
172
            self.renderer.clear(canvas, dims, theme);
173
        }
174
        for &y in &dirty {
175
            self.renderer.render_row(canvas, dims, grid, &frame, y);
176
        }
177
        // Draw the search prompt over the (now repainted) bottom row.
178
        if let Some(text) = &bar_text
179
            && dirty.contains(&(rows - 1))
180
        {
181
            self.renderer
182
                .render_search_bar(canvas, dims, theme, rows - 1, text);
183
        }
184
        // Draw the IME preedit inline over its (repainted) cursor row.
185
        for &y in &dirty {
186
            if let Some((col, text)) = &cur[y].preedit {
187
                self.renderer
188
                    .render_preedit(canvas, dims, theme, y, *col, text);
189
            }
190
        }
191
        // Draw URL hint labels on top, narrowing to those matching the input.
192
        if self.url_mode {
193
            for (hit, label) in self.url_hits.iter().zip(&self.url_labels) {
194
                if label.starts_with(&self.url_input) {
195
                    self.renderer
196
                        .render_label(canvas, dims, theme, hit.row, hit.col, label);
197
                }
198
            }
199
        }
200
        self.frames[idx].rows = cur;
201
202
        let surface = self.window.wl_surface();
203
        if let Err(err) = self.frames[idx].buffer.attach_to(surface) {
204
            tracing::error!("attach buffer: {err}");
205
            return;
206
        }
207
        // With a viewport the buffer is presented at the logical destination, so
208
        // its own scale stays 1; without one, fall back to integer buffer scale.
209
        if let Some(vp) = &self.viewport {
210
            surface.set_buffer_scale(1);
211
            vp.set_destination(self.width.max(1) as i32, self.height.max(1) as i32);
212
        } else {
213
            surface.set_buffer_scale((self.scale120 / 120).max(1) as i32);
214
        }
215
        if fresh {
216
            surface.damage_buffer(0, 0, w as i32, h as i32);
217
        } else {
218
            for &y in &dirty {
219
                let top = pad_y + y as i32 * m.height as i32;
220
                surface.damage_buffer(0, top, w as i32, m.height as i32);
221
            }
222
        }
223
        surface.frame(&self.qh, surface.clone());
224
        self.window.commit();
225
        self.frame_pending = true;
226
    }
227
}