use super::*; /// What determines one rendered row's pixels: its cells, the cursor on it, the /// selection span over it, and the blink phase. Two equal `RowSnap`s render /// identically, so a buffer holding an equal snapshot needs no repaint. #[derive(Clone, PartialEq, Debug)] struct RowSnap { cells: Vec, /// `(col, shape, focused)` when the cursor is drawn on this row. cursor: Option<(usize, CursorShape, bool)>, /// Inclusive selected column span on this row. sel: Option<(usize, usize)>, /// Search-match spans `(lo, hi, is_current)` highlighted on this row. search: Vec<(usize, usize, bool)>, /// Search-prompt text drawn over this row (only the bottom row, when active). overlay: Option, /// IME preedit `(start_col, text)` drawn inline over this row (cursor row). preedit: Option<(usize, String)>, /// Blink phase, but only varied when the row actually has blinking ink, so /// non-blinking rows stay equal across phase toggles. blink: bool, } /// One shm buffer plus the per-row snapshot of what it currently displays. #[derive(Debug)] pub(super) struct FrameBuf { buffer: Buffer, rows: Vec, } /// Snapshot the determinants of viewport row `y`'s pixels. fn row_snap(grid: &Grid, y: usize, focused: bool, blink_on: bool) -> RowSnap { let abs = grid.view_to_abs(y); let cells = grid.view_row(y).to_vec(); let cursor = if grid.view_at_bottom() && grid.cursor().1 == y { let visible = grid.cursor_visible() && (!grid.cursor_blink() || blink_on); visible.then(|| (grid.cursor().0, grid.cursor_shape(), focused)) } else { None }; let has_blink = cells .iter() .any(|c| c.flags.contains(crate::grid::Flags::BLINK)); RowSnap { cells, cursor, sel: grid.selection_span_on(abs), search: grid.search_spans_on(abs), overlay: None, preedit: None, blink: if has_blink { blink_on } else { true }, } } impl App { /// Render only the rows that changed since the chosen buffer last displayed /// them, damage just those rows, and commit with a frame-callback request. pub(super) fn present(&mut self) { self.needs_draw = false; // URL hint labels overlay the grid but are not part of the row snapshot, // so force a full redraw while the labels are showing. if self.url_mode { self.frames.clear(); } // Render into a buffer sized in physical pixels (logical × scale); the // viewport presents it back at the logical surface size. let (w, h) = self.phys_dims(); let m = self.renderer.metrics(); let (focused, blink_on) = (self.focused, self.blink_on); // A resize invalidates every buffer's contents and size. if self.buf_dims != (w, h) { self.frames.clear(); self.buf_dims = (w, h); } let Some(session) = self.session.as_ref() else { return; }; let grid = session.term.grid(); // The visual bell inverts fg/bg for the duration of the flash. let flashed = self.flashing.then(|| session.term.theme().inverted()); let theme = flashed.as_ref().unwrap_or(session.term.theme()); let rows = grid.rows(); let mut cur: Vec = (0..rows) .map(|y| row_snap(grid, y, focused, blink_on)) .collect(); // The search prompt occupies the bottom row while search mode is active. // Recording it in the snapshot keeps the row's damage/diff correct. let bar_text = if let Some(hex) = &self.unicode_input { Some(format!("unicode: U+{}", hex.to_uppercase())) } else { self.searching.then(|| { let (n, total) = grid.search_count(); format!( "search: {} [{n}/{total}]", grid.search_query().unwrap_or("") ) }) }; if let Some(text) = &bar_text && rows > 0 { cur[rows - 1].overlay = Some(text.clone()); } // The IME preedit is drawn inline at the cursor while composing. if !self.preedit.is_empty() && grid.view_at_bottom() { let (cx, cy) = grid.cursor(); if cy < rows { cur[cy].preedit = Some((cx, self.preedit.clone())); } } // Reuse a buffer the compositor has released, else grow the ring. let stride = w as i32 * 4; let mut idx = None; for i in 0..self.frames.len() { if self.pool.canvas(&self.frames[i].buffer).is_some() { idx = Some(i); break; } } let idx = match idx { Some(i) => i, None if self.frames.len() < MAX_BUFFERS => { match self .pool .create_buffer(w as i32, h as i32, stride, wl_shm::Format::Argb8888) { Ok((buffer, _)) => { self.frames.push(FrameBuf { buffer, rows: Vec::new(), }); self.frames.len() - 1 } Err(err) => { tracing::error!("allocate shm buffer: {err}"); return; } } } // All buffers are still held by the compositor; a release event will // wake us and `needs_draw` (re-set below) retries then. None => { self.needs_draw = true; return; } }; // Rows that differ from what this buffer last showed (all, if fresh). let prev = &self.frames[idx].rows; let dirty: Vec = (0..rows) .filter(|&y| prev.get(y) != Some(&cur[y])) .collect(); if dirty.is_empty() { return; } // A buffer used for the first time has uninitialized margins; paint the // whole thing (background + padding) once, then damage it in full below. let fresh = self.frames[idx].rows.is_empty(); let pad_y = self.to_phys(self.config.main.pad_y) as i32; let Some(canvas) = self.pool.canvas(&self.frames[idx].buffer) else { return; }; let dims = (w as usize, h as usize); let frame = crate::render::Frame { theme, focused, blink_on, hovered_link: self.hovered_link, }; if fresh { self.renderer.clear(canvas, dims, theme); } for &y in &dirty { self.renderer.render_row(canvas, dims, grid, &frame, y); } // Draw the search prompt over the (now repainted) bottom row. if let Some(text) = &bar_text && dirty.contains(&(rows - 1)) { self.renderer .render_search_bar(canvas, dims, theme, rows - 1, text); } // Draw the IME preedit inline over its (repainted) cursor row. for &y in &dirty { if let Some((col, text)) = &cur[y].preedit { self.renderer .render_preedit(canvas, dims, theme, y, *col, text); } } // Draw URL hint labels on top, narrowing to those matching the input. if self.url_mode { for (hit, label) in self.url_hits.iter().zip(&self.url_labels) { if label.starts_with(&self.url_input) { self.renderer .render_label(canvas, dims, theme, hit.row, hit.col, label); } } } self.frames[idx].rows = cur; let surface = self.window.wl_surface(); if let Err(err) = self.frames[idx].buffer.attach_to(surface) { tracing::error!("attach buffer: {err}"); return; } // With a viewport the buffer is presented at the logical destination, so // its own scale stays 1; without one, fall back to integer buffer scale. if let Some(vp) = &self.viewport { surface.set_buffer_scale(1); vp.set_destination(self.width.max(1) as i32, self.height.max(1) as i32); } else { surface.set_buffer_scale((self.scale120 / 120).max(1) as i32); } if fresh { surface.damage_buffer(0, 0, w as i32, h as i32); } else { for &y in &dirty { let top = pad_y + y as i32 * m.height as i32; surface.damage_buffer(0, top, w as i32, m.height as i32); } } surface.frame(&self.qh, surface.clone()); self.window.commit(); self.frame_pending = true; } }