forked from NotAShelf/beer
render: frame-paced presentation with per-row damage and blink
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4e925b4d1d904d9592060e968d84ec906a6a6964
This commit is contained in:
parent
7887420139
commit
f1c8271d31
4 changed files with 274 additions and 102 deletions
27
src/grid.rs
27
src/grid.rs
|
|
@ -140,6 +140,8 @@ pub struct Grid {
|
||||||
/// How many lines the viewport is scrolled back from the live bottom.
|
/// How many lines the viewport is scrolled back from the live bottom.
|
||||||
view_offset: usize,
|
view_offset: usize,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
|
/// Whether the cursor shape is a blinking variant (DECSCUSR odd codes).
|
||||||
|
cursor_blink: bool,
|
||||||
cursor_visible: bool,
|
cursor_visible: bool,
|
||||||
/// Application cursor-keys mode (DECCKM): arrows send SS3 instead of CSI.
|
/// Application cursor-keys mode (DECCKM): arrows send SS3 instead of CSI.
|
||||||
app_cursor: bool,
|
app_cursor: bool,
|
||||||
|
|
@ -177,6 +179,7 @@ impl Grid {
|
||||||
scrollback: VecDeque::new(),
|
scrollback: VecDeque::new(),
|
||||||
view_offset: 0,
|
view_offset: 0,
|
||||||
cursor_shape: CursorShape::default(),
|
cursor_shape: CursorShape::default(),
|
||||||
|
cursor_blink: false,
|
||||||
cursor_visible: true,
|
cursor_visible: true,
|
||||||
cursor_color: None,
|
cursor_color: None,
|
||||||
app_cursor: false,
|
app_cursor: false,
|
||||||
|
|
@ -265,6 +268,14 @@ impl Grid {
|
||||||
self.cursor_shape
|
self.cursor_shape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_cursor_blink(&mut self, blink: bool) {
|
||||||
|
self.cursor_blink = blink;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_blink(&self) -> bool {
|
||||||
|
self.cursor_blink
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_cursor_visible(&mut self, visible: bool) {
|
pub fn set_cursor_visible(&mut self, visible: bool) {
|
||||||
self.cursor_visible = visible;
|
self.cursor_visible = visible;
|
||||||
}
|
}
|
||||||
|
|
@ -819,6 +830,22 @@ impl Grid {
|
||||||
col >= lo && col <= hi
|
col >= lo && col <= hi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The inclusive `(lo, hi)` column span selected on absolute row `row`, if
|
||||||
|
/// any part of that row is selected.
|
||||||
|
pub fn selection_span_on(&self, row: usize) -> Option<(usize, usize)> {
|
||||||
|
let (start, end) = self.ordered_selection()?;
|
||||||
|
if row < start.row || row > end.row {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let lo = if row == start.row { start.col } else { 0 };
|
||||||
|
let hi = if row == end.row {
|
||||||
|
end.col
|
||||||
|
} else {
|
||||||
|
self.abs_row(row).len().saturating_sub(1)
|
||||||
|
};
|
||||||
|
Some((lo, hi))
|
||||||
|
}
|
||||||
|
|
||||||
/// The selected text, with trailing blanks trimmed per line and rows joined
|
/// The selected text, with trailing blanks trimmed per line and rows joined
|
||||||
/// by newlines. `None` if there is no selection.
|
/// by newlines. `None` if there is no selection.
|
||||||
pub fn selection_text(&self) -> Option<String> {
|
pub fn selection_text(&self) -> Option<String> {
|
||||||
|
|
|
||||||
106
src/render.rs
106
src/render.rs
|
|
@ -32,15 +32,6 @@ impl Canvas<'_> {
|
||||||
Some((y as usize * self.width + x as usize) * 4)
|
Some((y as usize * self.width + x as usize) * 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fill the whole buffer with one colour (fast path, no per-pixel bounds
|
|
||||||
/// checks).
|
|
||||||
fn clear(&mut self, c: Rgb) {
|
|
||||||
let bytes = [c.2, c.1, c.0, 0xff];
|
|
||||||
for px in self.pixels.chunks_exact_mut(4) {
|
|
||||||
px.copy_from_slice(&bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fill_rect(&mut self, x0: i32, y0: i32, w: u32, h: u32, c: Rgb) {
|
fn fill_rect(&mut self, x0: i32, y0: i32, w: u32, h: u32, c: Rgb) {
|
||||||
let x_start = x0.max(0) as usize;
|
let x_start = x0.max(0) as usize;
|
||||||
let x_end = ((x0 + w as i32).max(0) as usize).min(self.width);
|
let x_end = ((x0 + w as i32).max(0) as usize).min(self.width);
|
||||||
|
|
@ -110,76 +101,79 @@ impl Renderer {
|
||||||
self.fonts.metrics()
|
self.fonts.metrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compose `grid` into `pixels` (BGRA, `width`×`height` px). `focused`
|
/// Repaint a single grid row `y` into `pixels` (BGRA, `width`×`height` px):
|
||||||
/// selects a solid or hollow cursor.
|
/// clear the row band, fill backgrounds (and selection), draw glyphs and
|
||||||
pub fn render(
|
/// decorations, then the cursor if it sits on this row. `blink_on` is the
|
||||||
|
/// current blink phase; blinking cells and a blinking cursor vanish when it
|
||||||
|
/// is `false`. Painting one row at a time is what lets the caller damage
|
||||||
|
/// only the rows that actually changed.
|
||||||
|
pub fn render_row(
|
||||||
&mut self,
|
&mut self,
|
||||||
grid: &Grid,
|
|
||||||
pixels: &mut [u8],
|
pixels: &mut [u8],
|
||||||
width: usize,
|
dims: (usize, usize),
|
||||||
height: usize,
|
grid: &Grid,
|
||||||
|
y: usize,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
|
blink_on: bool,
|
||||||
) {
|
) {
|
||||||
|
let (width, height) = dims;
|
||||||
let mut canvas = Canvas {
|
let mut canvas = Canvas {
|
||||||
pixels,
|
pixels,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
};
|
};
|
||||||
canvas.clear(DEFAULT_BG);
|
|
||||||
|
|
||||||
let m = self.fonts.metrics();
|
let m = self.fonts.metrics();
|
||||||
let cols = grid.cols();
|
let cols = grid.cols();
|
||||||
|
let row_top = y as i32 * m.height as i32;
|
||||||
|
canvas.fill_rect(0, row_top, width as u32, m.height, DEFAULT_BG);
|
||||||
|
|
||||||
// Cell backgrounds: only paint cells that differ from the cleared
|
// Rows come through the scrollback viewport and may be shorter than
|
||||||
// default - most of a screen is default background. Rows come through
|
// `cols` after a resize, so clamp with `take`.
|
||||||
// the scrollback viewport and may be shorter than `cols` after a resize.
|
let abs = grid.view_to_abs(y);
|
||||||
for y in 0..grid.rows() {
|
let cells = grid.view_row(y);
|
||||||
let abs = grid.view_to_abs(y);
|
for (x, cell) in cells.iter().take(cols).enumerate() {
|
||||||
for (x, cell) in grid.view_row(y).iter().take(cols).enumerate() {
|
let bg = if grid.is_selected(abs, x) {
|
||||||
let bg = if grid.is_selected(abs, x) {
|
SELECTION_BG
|
||||||
SELECTION_BG
|
} else {
|
||||||
} else {
|
cell_colors(cell).1
|
||||||
cell_colors(cell).1
|
};
|
||||||
};
|
if bg != DEFAULT_BG {
|
||||||
if bg != DEFAULT_BG {
|
canvas.fill_rect(x as i32 * m.width as i32, row_top, m.width, m.height, bg);
|
||||||
let (px, py) = (x as i32 * m.width as i32, y as i32 * m.height as i32);
|
|
||||||
canvas.fill_rect(px, py, m.width, m.height, bg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for y in 0..grid.rows() {
|
for (x, cell) in cells.iter().take(cols).enumerate() {
|
||||||
for (x, cell) in grid.view_row(y).iter().take(cols).enumerate() {
|
if cell.flags.contains(Flags::WIDE_CONT) {
|
||||||
if cell.flags.contains(Flags::WIDE_CONT) {
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let (fg, _) = cell_colors(cell);
|
|
||||||
let origin_x = x as i32 * m.width as i32;
|
|
||||||
let cell_top = y as i32 * m.height as i32;
|
|
||||||
if cell.c != ' ' {
|
|
||||||
self.draw_glyph(
|
|
||||||
&mut canvas,
|
|
||||||
cell.c,
|
|
||||||
cell_style(cell),
|
|
||||||
origin_x,
|
|
||||||
cell_top,
|
|
||||||
fg,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
draw_decorations(&mut canvas, cell, origin_x, cell_top, m, fg);
|
|
||||||
}
|
}
|
||||||
|
if cell.flags.contains(Flags::BLINK) && !blink_on {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (fg, _) = cell_colors(cell);
|
||||||
|
let origin_x = x as i32 * m.width as i32;
|
||||||
|
if cell.c != ' ' {
|
||||||
|
self.draw_glyph(&mut canvas, cell.c, cell_style(cell), origin_x, row_top, fg);
|
||||||
|
}
|
||||||
|
draw_decorations(&mut canvas, cell, origin_x, row_top, m, fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The cursor belongs to the live screen; hide it while scrolled back.
|
// The cursor belongs to the live screen; hide it while scrolled back.
|
||||||
if grid.view_at_bottom() {
|
if grid.view_at_bottom() && grid.cursor().1 == y {
|
||||||
self.draw_cursor(&mut canvas, grid, m, focused);
|
self.draw_cursor(&mut canvas, grid, m, focused, blink_on);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the cursor: a solid block/underline/beam when focused, a hollow
|
/// Draw the cursor: a solid block/underline/beam when focused, a hollow
|
||||||
/// outline when not.
|
/// outline when not. A blinking cursor shape is only drawn while `blink_on`.
|
||||||
fn draw_cursor(&mut self, canvas: &mut Canvas, grid: &Grid, m: CellMetrics, focused: bool) {
|
fn draw_cursor(
|
||||||
if !grid.cursor_visible() {
|
&mut self,
|
||||||
|
canvas: &mut Canvas,
|
||||||
|
grid: &Grid,
|
||||||
|
m: CellMetrics,
|
||||||
|
focused: bool,
|
||||||
|
blink_on: bool,
|
||||||
|
) {
|
||||||
|
if !grid.cursor_visible() || (grid.cursor_blink() && !blink_on) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let (cx, cy) = grid.cursor();
|
let (cx, cy) = grid.cursor();
|
||||||
|
|
|
||||||
|
|
@ -473,11 +473,14 @@ impl Perform for Term {
|
||||||
}),
|
}),
|
||||||
'q' if intermediates.first() == Some(&b'>') => self.report_version(),
|
'q' if intermediates.first() == Some(&b'>') => self.report_version(),
|
||||||
'q' if intermediates.first() == Some(&b' ') => {
|
'q' if intermediates.first() == Some(&b' ') => {
|
||||||
self.grid.set_cursor_shape(match raw(params, 0) {
|
let code = raw(params, 0);
|
||||||
|
self.grid.set_cursor_shape(match code {
|
||||||
3 | 4 => CursorShape::Underline,
|
3 | 4 => CursorShape::Underline,
|
||||||
5 | 6 => CursorShape::Beam,
|
5 | 6 => CursorShape::Beam,
|
||||||
_ => CursorShape::Block,
|
_ => CursorShape::Block,
|
||||||
});
|
});
|
||||||
|
// Even codes are steady; 0/1 and other odd codes blink.
|
||||||
|
self.grid.set_cursor_blink(code == 0 || code % 2 == 1);
|
||||||
}
|
}
|
||||||
'p' if intermediates.contains(&b'$') => self.report_mode(params, private),
|
'p' if intermediates.contains(&b'$') => self.report_mode(params, private),
|
||||||
'n' => self.device_status(params),
|
'n' => self.device_status(params),
|
||||||
|
|
|
||||||
236
src/wayland.rs
236
src/wayland.rs
|
|
@ -9,12 +9,16 @@ use std::os::fd::OwnedFd;
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use calloop::generic::Generic;
|
use calloop::generic::Generic;
|
||||||
|
use calloop::timer::{TimeoutAction, Timer};
|
||||||
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction};
|
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction};
|
||||||
use calloop_wayland_source::WaylandSource;
|
use calloop_wayland_source::WaylandSource;
|
||||||
|
|
||||||
use crate::font::Fonts;
|
use crate::font::Fonts;
|
||||||
|
use crate::grid::{Cell, CursorShape, Grid};
|
||||||
use crate::pty::Pty;
|
use crate::pty::Pty;
|
||||||
use crate::render::Renderer;
|
use crate::render::Renderer;
|
||||||
use crate::vt::Term;
|
use crate::vt::Term;
|
||||||
|
|
@ -53,7 +57,10 @@ use smithay_client_toolkit::{
|
||||||
window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
|
window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shm::{Shm, ShmHandler, slot::SlotPool},
|
shm::{
|
||||||
|
Shm, ShmHandler,
|
||||||
|
slot::{Buffer, SlotPool},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
Connection, QueueHandle,
|
Connection, QueueHandle,
|
||||||
|
|
@ -86,6 +93,55 @@ fn pick_mime(mimes: &[String]) -> Option<String> {
|
||||||
/// Max gap between clicks counted as a multi-click (ms).
|
/// Max gap between clicks counted as a multi-click (ms).
|
||||||
const MULTI_CLICK_MS: u32 = 400;
|
const MULTI_CLICK_MS: u32 = 400;
|
||||||
|
|
||||||
|
/// Blink half-period: cells/cursor toggle visibility this often.
|
||||||
|
const BLINK_MS: u64 = 500;
|
||||||
|
|
||||||
|
/// Buffers kept for double/triple buffering before we wait for a release.
|
||||||
|
const MAX_BUFFERS: usize = 3;
|
||||||
|
|
||||||
|
/// 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)>,
|
||||||
|
/// 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)]
|
||||||
|
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),
|
||||||
|
blink: if has_blink { blink_on } else { true },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Default window size in pixels before the compositor suggests one.
|
/// Default window size in pixels before the compositor suggests one.
|
||||||
const DEFAULT_W: u32 = 800;
|
const DEFAULT_W: u32 = 800;
|
||||||
const DEFAULT_H: u32 = 600;
|
const DEFAULT_H: u32 = 600;
|
||||||
|
|
@ -159,22 +215,37 @@ pub fn run() -> anyhow::Result<ExitCode> {
|
||||||
title: None,
|
title: None,
|
||||||
width: DEFAULT_W,
|
width: DEFAULT_W,
|
||||||
height: DEFAULT_H,
|
height: DEFAULT_H,
|
||||||
dirty: false,
|
needs_draw: false,
|
||||||
|
frame_pending: false,
|
||||||
|
frames: Vec::new(),
|
||||||
|
buf_dims: (0, 0),
|
||||||
|
blink_on: true,
|
||||||
focused: true,
|
focused: true,
|
||||||
exit: false,
|
exit: false,
|
||||||
exit_code: ExitCode::SUCCESS,
|
exit_code: ExitCode::SUCCESS,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Block until an event (PTY output, input, configure) arrives - every
|
// Toggle the blink phase on a timer so blinking text and cursors animate.
|
||||||
// repaint is driven by one, so there is nothing to do on a timer.
|
let blink = Timer::from_duration(Duration::from_millis(BLINK_MS));
|
||||||
|
let blink_registered = event_loop
|
||||||
|
.handle()
|
||||||
|
.insert_source(blink, |_, _, app: &mut App| {
|
||||||
|
app.blink_on = !app.blink_on;
|
||||||
|
app.needs_draw = true;
|
||||||
|
TimeoutAction::ToDuration(Duration::from_millis(BLINK_MS))
|
||||||
|
});
|
||||||
|
if let Err(err) = blink_registered {
|
||||||
|
tracing::warn!("register blink timer: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each iteration blocks until an event (PTY output, input, configure, frame
|
||||||
|
// callback, blink) arrives, then presents at most one frame; bursts of PTY
|
||||||
|
// output between frame callbacks coalesce into a single repaint.
|
||||||
while !app.exit {
|
while !app.exit {
|
||||||
event_loop
|
event_loop
|
||||||
.dispatch(None, &mut app)
|
.dispatch(None, &mut app)
|
||||||
.context("dispatch event loop")?;
|
.context("dispatch event loop")?;
|
||||||
if app.dirty {
|
app.flush();
|
||||||
app.draw();
|
|
||||||
app.dirty = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(app.exit_code)
|
Ok(app.exit_code)
|
||||||
}
|
}
|
||||||
|
|
@ -244,8 +315,16 @@ struct App {
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
/// The grid changed and the window needs repainting.
|
/// The grid changed and the window wants repainting on the next frame.
|
||||||
dirty: bool,
|
needs_draw: bool,
|
||||||
|
/// A `wl_surface.frame` callback is in flight; defer drawing until it fires.
|
||||||
|
frame_pending: bool,
|
||||||
|
/// Double/triple-buffer ring, each tagged with the rows it currently shows.
|
||||||
|
frames: Vec<FrameBuf>,
|
||||||
|
/// Pixel size the `frames` buffers were allocated for.
|
||||||
|
buf_dims: (u32, u32),
|
||||||
|
/// Current blink phase, toggled by a timer; off hides blinking ink.
|
||||||
|
blink_on: bool,
|
||||||
/// Whether the toplevel currently has keyboard focus (drives the cursor).
|
/// Whether the toplevel currently has keyboard focus (drives the cursor).
|
||||||
focused: bool,
|
focused: bool,
|
||||||
exit: bool,
|
exit: bool,
|
||||||
|
|
@ -341,7 +420,7 @@ impl App {
|
||||||
-page
|
-page
|
||||||
};
|
};
|
||||||
session.term.scroll_view(delta);
|
session.term.scroll_view(delta);
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +434,7 @@ impl App {
|
||||||
{
|
{
|
||||||
session.term.scroll_to_bottom();
|
session.term.scroll_to_bottom();
|
||||||
session.term.grid_mut().clear_selection();
|
session.term.grid_mut().clear_selection();
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
if let Err(err) = write_all(session.pty.master(), &bytes) {
|
if let Err(err) = write_all(session.pty.master(), &bytes) {
|
||||||
tracing::warn!("write key to pty: {err}");
|
tracing::warn!("write key to pty: {err}");
|
||||||
}
|
}
|
||||||
|
|
@ -396,7 +475,7 @@ impl App {
|
||||||
_ => grid.start_selection(row, col),
|
_ => grid.start_selection(row, col),
|
||||||
}
|
}
|
||||||
self.selecting = true;
|
self.selecting = true;
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pointer motion during a drag: extend the selection head.
|
/// Pointer motion during a drag: extend the selection head.
|
||||||
|
|
@ -409,7 +488,7 @@ impl App {
|
||||||
};
|
};
|
||||||
if let Some(session) = self.session.as_mut() {
|
if let Some(session) = self.session.as_mut() {
|
||||||
session.term.grid_mut().extend_selection(row, col);
|
session.term.grid_mut().extend_selection(row, col);
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -534,7 +613,7 @@ impl App {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
session.term.scroll_to_bottom();
|
session.term.scroll_to_bottom();
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
let bracketed = session.term.grid().bracketed_paste();
|
let bracketed = session.term.grid().bracketed_paste();
|
||||||
// Strip control bytes a terminal must never receive raw from a paste;
|
// Strip control bytes a terminal must never receive raw from a paste;
|
||||||
// keep tab and newlines (CR is what the shell expects for Enter).
|
// keep tab and newlines (CR is what the shell expects for Enter).
|
||||||
|
|
@ -575,7 +654,7 @@ impl App {
|
||||||
self.window
|
self.window
|
||||||
.set_title(self.title.clone().unwrap_or_default());
|
.set_title(self.title.clone().unwrap_or_default());
|
||||||
}
|
}
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recompute the grid size for the current window and tell the grid and the
|
/// Recompute the grid size for the current window and tell the grid and the
|
||||||
|
|
@ -615,41 +694,106 @@ impl App {
|
||||||
self.exit = true;
|
self.exit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the grid into a fresh buffer and present it.
|
/// Present a frame if one is wanted and the compositor is ready for it.
|
||||||
fn draw(&mut self) {
|
/// Called after every event-loop wake; the frame-callback gate keeps draws
|
||||||
|
/// paced to the display instead of one per PTY read.
|
||||||
|
fn flush(&mut self) {
|
||||||
|
if self.needs_draw && !self.frame_pending && self.session.is_some() {
|
||||||
|
self.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render only the rows that changed since the chosen buffer last displayed
|
||||||
|
/// them, damage just those rows, and commit with a frame-callback request.
|
||||||
|
fn present(&mut self) {
|
||||||
|
self.needs_draw = false;
|
||||||
|
let (w, h) = (self.width, self.height);
|
||||||
|
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 {
|
let Some(session) = self.session.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let (w, h) = (self.width, self.height);
|
let grid = session.term.grid();
|
||||||
|
let rows = grid.rows();
|
||||||
|
let cur: Vec<RowSnap> = (0..rows)
|
||||||
|
.map(|y| row_snap(grid, y, focused, blink_on))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Reuse a buffer the compositor has released, else grow the ring.
|
||||||
let stride = w as i32 * 4;
|
let stride = w as i32 * 4;
|
||||||
|
let mut idx = None;
|
||||||
let (buffer, canvas) =
|
for i in 0..self.frames.len() {
|
||||||
match self
|
if self.pool.canvas(&self.frames[i].buffer).is_some() {
|
||||||
.pool
|
idx = Some(i);
|
||||||
.create_buffer(w as i32, h as i32, stride, wl_shm::Format::Argb8888)
|
break;
|
||||||
{
|
}
|
||||||
Ok(buf) => buf,
|
}
|
||||||
Err(err) => {
|
let idx = match idx {
|
||||||
tracing::error!("allocate shm buffer: {err}");
|
Some(i) => i,
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
self.renderer.render(
|
// Rows that differ from what this buffer last showed (all, if fresh).
|
||||||
session.term.grid(),
|
let prev = &self.frames[idx].rows;
|
||||||
canvas,
|
let dirty: Vec<usize> = (0..rows)
|
||||||
w as usize,
|
.filter(|&y| prev.get(y) != Some(&cur[y]))
|
||||||
h as usize,
|
.collect();
|
||||||
self.focused,
|
if dirty.is_empty() {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(canvas) = self.pool.canvas(&self.frames[idx].buffer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let dims = (w as usize, h as usize);
|
||||||
|
for &y in &dirty {
|
||||||
|
self.renderer
|
||||||
|
.render_row(canvas, dims, grid, y, focused, blink_on);
|
||||||
|
}
|
||||||
|
self.frames[idx].rows = cur;
|
||||||
|
|
||||||
let surface = self.window.wl_surface();
|
let surface = self.window.wl_surface();
|
||||||
if let Err(err) = buffer.attach_to(surface) {
|
if let Err(err) = self.frames[idx].buffer.attach_to(surface) {
|
||||||
tracing::error!("attach buffer: {err}");
|
tracing::error!("attach buffer: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
surface.damage_buffer(0, 0, w as i32, h as i32);
|
for &y in &dirty {
|
||||||
|
let top = 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.window.commit();
|
||||||
|
self.frame_pending = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -672,7 +816,11 @@ impl CompositorHandler for App {
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) {}
|
fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) {
|
||||||
|
// The compositor is ready for another frame; `flush` will repaint if the
|
||||||
|
// grid has changed since the last present.
|
||||||
|
self.frame_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
fn surface_enter(
|
fn surface_enter(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -716,7 +864,7 @@ impl WindowHandler for App {
|
||||||
} else {
|
} else {
|
||||||
self.resize_grid();
|
self.resize_grid();
|
||||||
}
|
}
|
||||||
self.draw();
|
self.needs_draw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -811,7 +959,7 @@ impl KeyboardHandler for App {
|
||||||
) {
|
) {
|
||||||
self.serial = serial;
|
self.serial = serial;
|
||||||
self.focused = true;
|
self.focused = true;
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leave(
|
fn leave(
|
||||||
|
|
@ -823,7 +971,7 @@ impl KeyboardHandler for App {
|
||||||
_: u32,
|
_: u32,
|
||||||
) {
|
) {
|
||||||
self.focused = false;
|
self.focused = false;
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn press_key(
|
fn press_key(
|
||||||
|
|
@ -936,7 +1084,7 @@ impl PointerHandler for App {
|
||||||
let delta = if raw < 0.0 { lines } else { -lines };
|
let delta = if raw < 0.0 { lines } else { -lines };
|
||||||
if let Some(session) = self.session.as_mut() {
|
if let Some(session) = self.session.as_mut() {
|
||||||
session.term.scroll_view(delta);
|
session.term.scroll_view(delta);
|
||||||
self.dirty = true;
|
self.needs_draw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue