forked from NotAShelf/beer
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I0f58c82752b9d7a8df35fe78f034c0be6a6a6964
231 lines
8.7 KiB
Rust
231 lines
8.7 KiB
Rust
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<Cell>,
|
||
/// `(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<String>,
|
||
/// 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<RowSnap>,
|
||
}
|
||
|
||
/// 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<RowSnap> = (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<usize> = (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;
|
||
}
|
||
}
|