feat(renderer): port the generic renderer, renderer thread and state
Ports renderer/{generic,cell,row,link,State,Overlay,image,Thread}.zig:
- generic.rs builds GPU cell buffers from terminal RenderState each
frame: backgrounds, shaped text runs with glyph atlases and nerd font
constraints, all cursor styles, decorations via sprites, selection and
search colors, min-contrast, bold/faint/blink handling, preedit,
padding extension, background image, custom shader uniforms, link
hover underlines, scrollbar and health reporting.
- The renderer thread updates CPU-side state only and asks the host to
redraw; the app runtime calls draw_frame from the GLArea render signal
(the GL context lives on the GTK thread, as in Ghostty).
- ghostty_renderer::state::{State, SharedState} is the canonical shared
renderer state (termio's interim copy will switch to it).
Deviations: overlay uses a direct RGBA rasterizer instead of z2d; bg
images decode with png/jpeg-decoder instead of wuffs; zero-length link
matches advance by one char (Zig would loop forever).
Kitty image rendering is implemented in image::State but not yet called
from generic::update_kitty (raced with the terminal-side wiring).
Test Plan:
- cargo test -p ghostty-renderer (69 unit + 9 headless EGL tests that
render a Terminal through the full pipeline and check pixels)
This commit is contained in:
1 parent
25da94fb75
commit
2fbb43317c
16 files changed
+7491
-128
No files matched your search
@@ -0,0 +1,210 @@
|
||||
//! The render state that is given to a renderer (`renderer/State.zig`).
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ghostty_input::Mods;
|
||||
use ghostty_terminal::point::Coordinate;
|
||||
use ghostty_terminal::size::CellCountInt;
|
||||
use ghostty_terminal::terminal::Terminal;
|
||||
|
||||
use crate::overlay::Feature as OverlayFeature;
|
||||
|
||||
/// The state shared between the IO side, the surface and the renderer.
|
||||
///
|
||||
/// Zig passes a `*Mutex` plus pointers to the terminal and friends. Rust
|
||||
/// folds them into one value behind one mutex: everything in here must only
|
||||
/// be read or written while holding [`SharedState`]'s lock (the "renderer
|
||||
/// mutex" of Ghostty). The termio thread parses into `terminal`, the surface
|
||||
/// updates `preedit`/`mouse`, and the renderer thread reads all of it in
|
||||
/// `update_frame`.
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
/// The terminal data.
|
||||
pub terminal: Terminal,
|
||||
|
||||
/// The overlay features currently requested by the terminal inspector
|
||||
/// (Zig: `inspector: ?*Inspector` and `inspector.rendererInfo()
|
||||
/// .overlayFeatures()`). Empty while the inspector is not active.
|
||||
// TODO(integration): the inspector is deferred (Phase 7); it fills this.
|
||||
pub overlay_features: Vec<OverlayFeature>,
|
||||
|
||||
/// Dead key state. This will render the current dead key preedit text
|
||||
/// over the cursor. This currently only ever renders a single codepoint.
|
||||
/// Preedit can in theory be multiple codepoints long but that is left as
|
||||
/// a future exercise.
|
||||
pub preedit: Option<Preedit>,
|
||||
|
||||
/// Mouse state. This only contains state relevant to what renderers
|
||||
/// need about the mouse.
|
||||
pub mouse: Mouse,
|
||||
}
|
||||
|
||||
/// The shared, mutex-protected renderer state.
|
||||
pub type SharedState = Arc<Mutex<State>>;
|
||||
|
||||
impl State {
|
||||
pub fn new(terminal: Terminal) -> State {
|
||||
State {
|
||||
terminal,
|
||||
overlay_features: Vec::new(),
|
||||
preedit: None,
|
||||
mouse: Mouse::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap this state into the shared form used by the renderer thread.
|
||||
pub fn into_shared(self) -> SharedState {
|
||||
Arc::new(Mutex::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Mouse {
|
||||
/// The point on the viewport where the mouse currently is. We use
|
||||
/// viewport points to avoid the complexity of mapping the mouse to
|
||||
/// the renderer state.
|
||||
pub point: Option<Coordinate>,
|
||||
|
||||
/// The mods that are currently active for the last mouse event.
|
||||
/// This could really just be mods in general and we probably will
|
||||
/// move it out of mouse state at some point.
|
||||
pub mods: Mods,
|
||||
}
|
||||
|
||||
/// The pre-edit state. See Surface.preeditCallback for more information.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct Preedit {
|
||||
/// The codepoints to render as preedit text.
|
||||
pub codepoints: Vec<PreeditCodepoint>,
|
||||
}
|
||||
|
||||
/// A single codepoint to render as preedit text.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PreeditCodepoint {
|
||||
pub codepoint: u32,
|
||||
pub wide: bool,
|
||||
}
|
||||
|
||||
/// The result of [`Preedit::range`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PreeditRange {
|
||||
pub start: CellCountInt,
|
||||
pub end: CellCountInt,
|
||||
pub cp_offset: usize,
|
||||
}
|
||||
|
||||
impl Preedit {
|
||||
/// The width in cells of all codepoints in the preedit.
|
||||
pub fn width(&self) -> usize {
|
||||
self.codepoints
|
||||
.iter()
|
||||
.map(|cp| if cp.wide { 2 } else { 1 })
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Range returns the start and end x position of the preedit text
|
||||
/// along with any codepoint offset necessary to fit the preedit
|
||||
/// into the available space.
|
||||
pub fn range(&self, start: CellCountInt, max: CellCountInt) -> PreeditRange {
|
||||
// If our width is greater than the number of cells we have
|
||||
// then we need to adjust our codepoint start to a point where
|
||||
// our width would be less than the number of cells we have.
|
||||
let (w, cp_offset) = 'width: {
|
||||
// max is inclusive, so we need to add 1 to it.
|
||||
let max_width = max - start + 1;
|
||||
|
||||
// Rebuild our width in reverse order. This is because we want
|
||||
// to offset by the end cells, not the start cells (if we have to).
|
||||
let mut w: CellCountInt = 0;
|
||||
for i in 0..self.codepoints.len() {
|
||||
let reverse_i = self.codepoints.len() - i - 1;
|
||||
let cp = self.codepoints[reverse_i];
|
||||
w += if cp.wide { 2 } else { 1 };
|
||||
if w > max_width {
|
||||
break 'width (w, reverse_i);
|
||||
}
|
||||
}
|
||||
|
||||
// Width fit in the max width so no offset necessary.
|
||||
(w, 0)
|
||||
};
|
||||
|
||||
// If our preedit goes off the end of the screen, we adjust it so
|
||||
// that it shifts left.
|
||||
let end = start + w;
|
||||
let start_offset = end.saturating_sub(max);
|
||||
PreeditRange {
|
||||
start: start.saturating_sub(start_offset),
|
||||
end: end.saturating_sub(start_offset),
|
||||
cp_offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cp(c: char, wide: bool) -> PreeditCodepoint {
|
||||
PreeditCodepoint {
|
||||
codepoint: c as u32,
|
||||
wide,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preedit_width() {
|
||||
let p = Preedit {
|
||||
codepoints: vec![cp('a', false), cp('b', true)],
|
||||
};
|
||||
assert_eq!(p.width(), 3);
|
||||
assert_eq!(Preedit::default().width(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preedit_range_fits() {
|
||||
let p = Preedit {
|
||||
codepoints: vec![cp('a', false), cp('b', true)],
|
||||
};
|
||||
let r = p.range(2, 9);
|
||||
assert_eq!(
|
||||
r,
|
||||
PreeditRange {
|
||||
start: 2,
|
||||
end: 5,
|
||||
cp_offset: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preedit_range_shifts_left_at_edge() {
|
||||
let p = Preedit {
|
||||
codepoints: vec![cp('a', false), cp('b', false), cp('c', false)],
|
||||
};
|
||||
// Start at the last column of a 10 column screen (max = 9).
|
||||
let r = p.range(8, 9);
|
||||
// Width is 3 > max_width (2): the span shifts left so it ends at max.
|
||||
assert_eq!(
|
||||
r,
|
||||
PreeditRange {
|
||||
start: 6,
|
||||
end: 9,
|
||||
cp_offset: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preedit_range_too_wide_for_screen() {
|
||||
let p = Preedit {
|
||||
codepoints: vec![cp('a', true), cp('b', true), cp('c', true)],
|
||||
};
|
||||
// 4 columns available starting at 0 (max = 3): width 6 > 4.
|
||||
let r = p.range(0, 3);
|
||||
// Walking in reverse: c(2) b(4) a(6>4) breaks at index 0.
|
||||
assert_eq!(r.cp_offset, 0);
|
||||
assert_eq!(r.end, 3);
|
||||
assert_eq!(r.start, 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user