brewery
notashelf /
fb590c1645035cc2537f826f025ac7ea12f5e514

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/wayland/rendering.rsRust231 lines8.7 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 = if let Some(hex) = &self.unicode_input {
91
            Some(format!("unicode: U+{}", hex.to_uppercase()))
92
        } else {
93
            self.searching.then(|| {
94
                let (n, total) = grid.search_count();
95
                format!(
96
                    "search: {}  [{n}/{total}]",
97
                    grid.search_query().unwrap_or("")
98
                )
99
            })
100
        };
101
        if let Some(text) = &bar_text
102
            && rows > 0
103
        {
104
            cur[rows - 1].overlay = Some(text.clone());
105
        }
106
107
        // The IME preedit is drawn inline at the cursor while composing.
108
        if !self.preedit.is_empty() && grid.view_at_bottom() {
109
            let (cx, cy) = grid.cursor();
110
            if cy < rows {
111
                cur[cy].preedit = Some((cx, self.preedit.clone()));
112
            }
113
        }
114
115
        // Reuse a buffer the compositor has released, else grow the ring.
116
        let stride = w as i32 * 4;
117
        let mut idx = None;
118
        for i in 0..self.frames.len() {
119
            if self.pool.canvas(&self.frames[i].buffer).is_some() {
120
                idx = Some(i);
121
                break;
122
            }
123
        }
124
        let idx = match idx {
125
            Some(i) => i,
126
            None if self.frames.len() < MAX_BUFFERS => {
127
                match self
128
                    .pool
129
                    .create_buffer(w as i32, h as i32, stride, wl_shm::Format::Argb8888)
130
                {
131
                    Ok((buffer, _)) => {
132
                        self.frames.push(FrameBuf {
133
                            buffer,
134
                            rows: Vec::new(),
135
                        });
136
                        self.frames.len() - 1
137
                    }
138
                    Err(err) => {
139
                        tracing::error!("allocate shm buffer: {err}");
140
                        return;
141
                    }
142
                }
143
            }
144
            // All buffers are still held by the compositor; a release event will
145
            // wake us and `needs_draw` (re-set below) retries then.
146
            None => {
147
                self.needs_draw = true;
148
                return;
149
            }
150
        };
151
152
        // Rows that differ from what this buffer last showed (all, if fresh).
153
        let prev = &self.frames[idx].rows;
154
        let dirty: Vec<usize> = (0..rows)
155
            .filter(|&y| prev.get(y) != Some(&cur[y]))
156
            .collect();
157
        if dirty.is_empty() {
158
            return;
159
        }
160
161
        // A buffer used for the first time has uninitialized margins; paint the
162
        // whole thing (background + padding) once, then damage it in full below.
163
        let fresh = self.frames[idx].rows.is_empty();
164
        let pad_y = self.to_phys(self.config.main.pad_y) as i32;
165
        let Some(canvas) = self.pool.canvas(&self.frames[idx].buffer) else {
166
            return;
167
        };
168
        let dims = (w as usize, h as usize);
169
        let frame = crate::render::Frame {
170
            theme,
171
            focused,
172
            blink_on,
173
            hovered_link: self.hovered_link,
174
        };
175
        if fresh {
176
            self.renderer.clear(canvas, dims, theme);
177
        }
178
        for &y in &dirty {
179
            self.renderer.render_row(canvas, dims, grid, &frame, y);
180
        }
181
        // Draw the search prompt over the (now repainted) bottom row.
182
        if let Some(text) = &bar_text
183
            && dirty.contains(&(rows - 1))
184
        {
185
            self.renderer
186
                .render_search_bar(canvas, dims, theme, rows - 1, text);
187
        }
188
        // Draw the IME preedit inline over its (repainted) cursor row.
189
        for &y in &dirty {
190
            if let Some((col, text)) = &cur[y].preedit {
191
                self.renderer
192
                    .render_preedit(canvas, dims, theme, y, *col, text);
193
            }
194
        }
195
        // Draw URL hint labels on top, narrowing to those matching the input.
196
        if self.url_mode {
197
            for (hit, label) in self.url_hits.iter().zip(&self.url_labels) {
198
                if label.starts_with(&self.url_input) {
199
                    self.renderer
200
                        .render_label(canvas, dims, theme, hit.row, hit.col, label);
201
                }
202
            }
203
        }
204
        self.frames[idx].rows = cur;
205
206
        let surface = self.window.wl_surface();
207
        if let Err(err) = self.frames[idx].buffer.attach_to(surface) {
208
            tracing::error!("attach buffer: {err}");
209
            return;
210
        }
211
        // With a viewport the buffer is presented at the logical destination, so
212
        // its own scale stays 1; without one, fall back to integer buffer scale.
213
        if let Some(vp) = &self.viewport {
214
            surface.set_buffer_scale(1);
215
            vp.set_destination(self.width.max(1) as i32, self.height.max(1) as i32);
216
        } else {
217
            surface.set_buffer_scale((self.scale120 / 120).max(1) as i32);
218
        }
219
        if fresh {
220
            surface.damage_buffer(0, 0, w as i32, h as i32);
221
        } else {
222
            for &y in &dirty {
223
                let top = pad_y + y as i32 * m.height as i32;
224
                surface.damage_buffer(0, top, w as i32, m.height as i32);
225
            }
226
        }
227
        surface.frame(&self.qh, surface.clone());
228
        self.window.commit();
229
        self.frame_pending = true;
230
    }
231
}