treewide: split terminal core modules

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9cace0b7c6995c0fca21ff2cf465ae1f6a6a6964
This commit is contained in:
raf 2026-06-25 14:42:15 +03:00
commit 5cba919c78
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
13 changed files with 1876 additions and 1700 deletions

227
src/wayland/rendering.rs Normal file
View file

@ -0,0 +1,227 @@
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 = 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;
}
}