From 720b4bcbe63016365ba3d83fc10629071d9e2df6 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Sat, 25 Apr 2026 21:09:11 +0200 Subject: [PATCH] fix cursor rendering --- frontends/rioterm/src/grid_emit.rs | 363 ++++++++++++++++-- frontends/rioterm/src/hints.rs | 6 +- frontends/rioterm/src/layout/mod.rs | 4 +- frontends/rioterm/src/mouse/mod.rs | 10 +- frontends/rioterm/src/renderer/island.rs | 6 +- frontends/rioterm/src/renderer/mod.rs | 30 +- frontends/rioterm/src/screen/mod.rs | 169 ++++++-- .../src/ansi/kitty_graphics_protocol.rs | 10 +- rio-backend/src/ansi/kitty_virtual.rs | 18 +- rio-backend/src/config/hints.rs | 10 +- rio-backend/src/config/mod.rs | 12 +- rio-backend/src/crosswords/mod.rs | 82 ++-- rio-backend/src/crosswords/square.rs | 44 +-- .../src/platform_impl/macos/app_delegate.rs | 60 +-- sugarloaf/src/context/metal.rs | 4 +- sugarloaf/src/font/macos.rs | 6 +- sugarloaf/src/font/nerd_font_attributes.rs | 2 +- sugarloaf/src/font/windows.rs | 2 +- sugarloaf/src/font_cache.rs | 2 +- sugarloaf/src/grid/cell.rs | 28 +- sugarloaf/src/grid/cpu.rs | 27 ++ sugarloaf/src/grid/metal.rs | 94 ++++- sugarloaf/src/grid/mod.rs | 54 ++- sugarloaf/src/grid/shaders/grid.metal | 108 +++--- sugarloaf/src/grid/shaders/grid.wgsl | 89 ++--- .../src/grid/shaders/grid_text.vert.glsl | 6 +- sugarloaf/src/grid/vulkan.rs | 45 +++ sugarloaf/src/grid/webgpu.rs | 47 ++- sugarloaf/src/layout/content.rs | 24 +- sugarloaf/src/renderer/image.metal | 30 +- sugarloaf/src/renderer/mod.rs | 36 +- sugarloaf/src/renderer/renderer.metal | 48 +-- 32 files changed, 1045 insertions(+), 431 deletions(-) diff --git a/frontends/rioterm/src/grid_emit.rs b/frontends/rioterm/src/grid_emit.rs index 51a3af89..05dfe273 100644 --- a/frontends/rioterm/src/grid_emit.rs +++ b/frontends/rioterm/src/grid_emit.rs @@ -20,7 +20,7 @@ //! Both populate the same `ShapedGlyph` shape and route into the same //! `GridRenderer` atlases via the same emit loop. //! -//! Mirrors Ghostty's `font::shaper::run::RunIterator` (`run.zig`). +//! `font::shaper::run::RunIterator`. use rio_backend::config::colors::term::TermColors; use rio_backend::crosswords::grid::row::Row; @@ -94,8 +94,8 @@ fn cell_in_row_sel(row_sel: Option, col: u16) -> bool { } } -/// Search-hint category at a cell. Matches Ghostty's `HighlightTag` -/// (`ghostty/src/renderer/generic.zig:240`) — we use the same two-way +/// Search-hint category at a cell. `HighlightTag` +/// — we use the same two-way /// split so `search_focused_match_background` can override the regular /// match color on the currently-focused hit. /// @@ -125,7 +125,7 @@ pub struct RowHint { /// /// `focused_match` is pushed first so it wins `cell_in_row_hints` /// iteration order when it overlaps another match — same precedence -/// as Ghostty (`generic.zig:1330-1353`: "The order below matters. +/// as (`generic.zig:1330-1353`: "The order below matters. /// Highlights added earlier will take priority"). pub fn row_hints_for( hint_matches: Option<&[Match]>, @@ -256,7 +256,7 @@ use rio_backend::sugarloaf::grid::{ AtlasSlot, CellBg, CellText, GlyphKey, GridRenderer, RasterizedGlyph, }; -// Bg + shared helpers +// Bg + shared helpers pub fn cell_fg( sq: Square, @@ -272,10 +272,10 @@ pub fn cell_fg( normalized_to_u8(color) } -/// Foreground for a selected cell. Mirrors Ghostty's selection-fg -/// rule (`generic.zig:2867`): use the configured `selection-foreground` +/// Foreground for a selected cell. selection-fg +/// rule: use the configured `selection-foreground` /// unless the user asked to keep the cell's own fg (Rio's -/// `ignore-selection-foreground-color`). Ghostty falls back to +/// `ignore-selection-foreground-color`). falls back to /// `state.colors.background` when no color is configured; Rio always /// has a default selection_foreground populated in its theme, so we /// use it directly. @@ -293,9 +293,9 @@ pub fn cell_fg_selected( } } -// Decoration sprites (underlines, strikethrough) +// Decoration sprites (underlines, strikethrough) // -// Ghostty pre-rasterizes underline/strikethrough sprites into the +// pre-rasterizes underline/strikethrough sprites into the // grayscale atlas and emits them as regular `CellText` entries // (`ghostty/src/font/sprite/draw/special.zig`, // `ghostty/src/renderer/generic.zig:3074`). We do the same: one sprite @@ -317,10 +317,299 @@ enum DecorationStyle { /// Sentinel font_id base for decoration sprites. Real font_ids come /// from sugarloaf's font library which packs into usize indices /// starting at 0; 0xFFFF_FF00+ is far outside that range. Matches -/// Ghostty's `font.sprite_index` idea (`font/sprite.zig:17`). +/// `font.sprite_index` idea. const DECORATION_FONT_ID_BASE: u32 = 0xFFFF_FF00; -/// Underline thickness in physical pixels. Matches Ghostty's fallback +/// Sentinel font_id base for cursor sprites. Distinct from the +/// decoration range so the two never collide in the atlas +/// hash-key space. +const CURSOR_FONT_ID_BASE: u32 = 0xFFFF_FE00; + +/// Cursor sprite styles. `font.Sprite::cursor_*` +///. Each variant maps to a distinct +/// rasterized bitmap stored in the grid's grayscale atlas. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u32)] +enum CursorSpriteStyle { + /// Full-cell filled rectangle. Drawn UNDER text via slot 0 so + /// inverted text composites on top. + Block = 0, + /// Outlined rectangle (focused-cell border for inactive panes). + Hollow = 1, + /// Vertical bar, `thickness` px wide, centered on the LEFT edge + /// of the cursor cell (straddles the cell boundary). + Bar = 2, + /// Horizontal bar at the underline position, `thickness` px tall. + Underline = 3, +} + +impl CursorSpriteStyle { + /// Block cursors land in `fg_rows[0]` so glyphs draw on top + /// (the text shader's fg-swap handles the inverted character). + /// Everything else lands in the non-block slot to overlay text. + #[inline] + fn is_block_slot(self) -> bool { + matches!(self, CursorSpriteStyle::Block) + } +} + +/// Top-level cursor render decision. +/// `renderer::cursor::Style` enum — a +/// superset of the terminal's cursor shapes that adds the +/// inactive-pane variant. Lock isn't implemented yet; password-input +/// detection would need DEC mode 2004 plumbing in the parser. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CursorRenderStyle { + /// Active focused block, painted via uniforms (text inverts). + /// Also emits a `cursor_rect` sprite into slot 0 for parity with + /// . + Block, + /// Outlined rectangle for inactive split panels. + BlockHollow, + /// Vertical bar (`beam` in rio's config; calls it `bar`). + Bar, + /// Underscore at the cell baseline. + Underline, +} + +/// Inputs to the cursor-style decision. +/// `renderer::cursor::StyleOptions`. +pub struct CursorRenderInputs { + /// `false` when DECTCEM hides the cursor. + pub visible: bool, + /// `true` when this panel currently has focus. + pub focused: bool, + /// `true` for the visible half of a blink cycle. Pass `true` + /// when blink is disabled. + pub blink_visible: bool, + /// `true` when the cursor is blinking (DEC blinking shape, or + /// SGR cursor blink). + pub blinking: bool, + /// `true` while an IME pre-edit string is active. Forces block + /// regardless of the configured shape so the user can tell IME + /// is taking input. + pub preedit: bool, + /// The terminal-side configured cursor shape (block / underline / + /// beam / hidden). + pub shape: rio_backend::ansi::CursorShape, +} + +/// Decide which cursor variant to render this frame, or `None` to +/// skip emission entirely (hidden cursor / blink-off half-frame). +/// Strict priority order mirrors : +/// preedit > visibility > focused > blink > terminal shape. +pub fn cursor_render_style(opts: CursorRenderInputs) -> Option { + use rio_backend::ansi::CursorShape; + if opts.preedit { + return Some(CursorRenderStyle::Block); + } + if !opts.visible || opts.shape == CursorShape::Hidden { + return None; + } + if !opts.focused { + return Some(CursorRenderStyle::BlockHollow); + } + if opts.blinking && !opts.blink_visible { + return None; + } + Some(match opts.shape { + CursorShape::Block => CursorRenderStyle::Block, + CursorShape::Underline => CursorRenderStyle::Underline, + CursorShape::Beam => CursorRenderStyle::Bar, + // Hidden was filtered out by the visibility check above. + CursorShape::Hidden => unreachable!("hidden shape is filtered above"), + }) +} + +impl CursorRenderStyle { + #[inline] + fn sprite(self) -> CursorSpriteStyle { + match self { + CursorRenderStyle::Block => CursorSpriteStyle::Block, + CursorRenderStyle::BlockHollow => CursorSpriteStyle::Hollow, + CursorRenderStyle::Bar => CursorSpriteStyle::Bar, + CursorRenderStyle::Underline => CursorSpriteStyle::Underline, + } + } +} + +/// Cursor stroke thickness in physical px. pulls this from +/// font metrics (`metrics.cursor_thickness`); we approximate from +/// cell height. Capped at 2 px so deeply-zoomed cells don't get a +/// chunky frame / fat bar instead of a cursor hint. +#[inline] +fn cursor_thickness(cell_h: u32) -> u32 { + (cell_h / 16).clamp(1, 2) +} + +/// Per-style sprite bitmap + bearings. Top-of-sprite bearing is +/// `cell_h` for vertical-fill sprites (block / hollow / bar) so the +/// sprite's top edge aligns with the cell top; underline uses a +/// smaller bearing so the sprite sits near the cell baseline. +fn rasterize_cursor( + style: CursorSpriteStyle, + cell_w: u32, + cell_h: u32, + thickness: u32, +) -> (Vec, u16, u16, i16, i16) { + let t = thickness.max(1); + match style { + CursorSpriteStyle::Block => { + // Full-cell fill. + let bytes = vec![0xFFu8; (cell_w * cell_h) as usize]; + ( + bytes, + cell_w.min(u16::MAX as u32) as u16, + cell_h.min(u16::MAX as u32) as u16, + 0, + cell_h.min(i16::MAX as u32) as i16, + ) + } + CursorSpriteStyle::Hollow => { + // Filled rect minus inset rect (= border ring). Same as + // `cursor_hollow_rect`. + let row_w = cell_w as usize; + let h = cell_h as usize; + let mut bytes = vec![0u8; row_w * h]; + let ti = (t as usize).max(1); + for row in 0..ti.min(h) { + let s = row * row_w; + bytes[s..s + row_w].fill(0xFF); + } + for row in h.saturating_sub(ti)..h { + let s = row * row_w; + bytes[s..s + row_w].fill(0xFF); + } + for row in ti..h.saturating_sub(ti) { + let s = row * row_w; + for col in 0..ti.min(row_w) { + bytes[s + col] = 0xFF; + } + for col in row_w.saturating_sub(ti)..row_w { + bytes[s + col] = 0xFF; + } + } + ( + bytes, + cell_w.min(u16::MAX as u32) as u16, + cell_h.min(u16::MAX as u32) as u16, + 0, + cell_h.min(i16::MAX as u32) as i16, + ) + } + CursorSpriteStyle::Bar => { + // Vertical bar `t` px wide, full cell height. Negative + // bearing_x straddles the cell boundary so a bar between + // cells `n-1` and `n` looks right. uses + // `x = -(thickness + 1) / 2`. + let bytes = vec![0xFFu8; (t * cell_h) as usize]; + let bearing_x = -((t as i16 + 1) / 2); + ( + bytes, + t.min(u16::MAX as u32) as u16, + cell_h.min(u16::MAX as u32) as u16, + bearing_x, + cell_h.min(i16::MAX as u32) as i16, + ) + } + CursorSpriteStyle::Underline => { + // Horizontal bar at the underline position. Reuse the + // SGR-underline gap formula so the cursor underline sits + // at the same baseline as a regular underline. + let bytes = vec![0xFFu8; (cell_w * t) as usize]; + // The text shader's `glyph_y = cell_pos.y + cell_h - + // bearing_y` puts the sprite top at + // `cell_h - (t + gap)`, leaving a `gap` of empty rows + // below the underline. + let bearing_y = (t + underline_gap_below(cell_h)) as i16; + ( + bytes, + cell_w.min(u16::MAX as u32) as u16, + t.min(u16::MAX as u32) as u16, + 0, + bearing_y, + ) + } + } +} + +/// Lookup or insert a cursor sprite. `size_bucket` packs `(thickness, +/// cell_h)` so a font-size or DPI change invalidates the cached +/// sprite. `cell_w` is the glyph_id so wide-cell sprites (CJK +/// double-width) get their own slot. +fn ensure_cursor_sprite_slot( + grid: &mut GridRenderer, + style: CursorSpriteStyle, + cell_w: u32, + cell_h: u32, + thickness: u32, +) -> Option { + let key = GlyphKey { + font_id: CURSOR_FONT_ID_BASE + style as u32, + glyph_id: cell_w, + size_bucket: ((thickness as u16 & 0xF) << 12) | (cell_h.min(0xFFF) as u16), + }; + if let Some(slot) = grid.lookup_glyph(key) { + return Some(slot); + } + let (bytes, w, h, bearing_x, bearing_y) = + rasterize_cursor(style, cell_w, cell_h, thickness); + grid.insert_glyph( + key, + RasterizedGlyph { + width: w, + height: h, + bearing_x, + bearing_y, + bytes: &bytes, + }, + ) +} + +/// Emit a cursor sprite into the appropriate `fg_rows` slot. Caller +/// is responsible for clearing the OTHER slot (so a previous-frame +/// block doesn't linger when this frame draws a hollow, etc.) — see +/// `grid.clear_cursor()`. `addCursor` +///. +pub fn emit_cursor_sprite( + grid: &mut GridRenderer, + style: CursorRenderStyle, + col: u16, + row: u16, + color: [u8; 4], + cell_w: u32, + cell_h: u32, +) { + let sprite = style.sprite(); + let thickness = cursor_thickness(cell_h); + let Some(slot) = ensure_cursor_sprite_slot(grid, sprite, cell_w, cell_h, thickness) + else { + return; + }; + if slot.w == 0 || slot.h == 0 { + return; + } + let cursor_cell = CellText { + glyph_pos: [slot.x as u32, slot.y as u32], + glyph_size: [slot.w as u32, slot.h as u32], + bearings: [slot.bearing_x, slot.bearing_y], + grid_pos: [col, row], + color, + atlas: CellText::ATLAS_GRAYSCALE, + // Marks this as "the cursor itself" so the text shader's + // fg-swap skips it (the sprite paints in `color` directly, + // not in `cursor_color` from the uniforms). + bools: CellText::BOOL_IS_CURSOR_GLYPH, + _pad: [0, 0], + }; + if sprite.is_block_slot() { + grid.set_block_cursor(&[cursor_cell]); + } else { + grid.set_non_block_cursor(&[cursor_cell]); + } +} + +/// Underline thickness in physical pixels. fallback /// (15% of ex-height, min 1px) when the font doesn't expose /// `underline_thickness` — we don't thread per-font metrics through to /// decorations because runs can mix fonts inside a row. 0.075 * size_px @@ -332,7 +621,7 @@ fn decoration_thickness(size_px: f32) -> u32 { /// Offset (in pixels, from cell bottom) at which the BOTTOM of an /// underline sits. Small gap so underlines don't merge with the row -/// below. Mirrors the spirit of Ghostty's `underline_position` but +/// below. Mirrors the spirit of `underline_position` but /// simplified — we don't have per-font metrics here. #[inline] fn underline_gap_below(cell_h: u32) -> u32 { @@ -402,7 +691,7 @@ fn rasterize_decoration( // segment widths differ by a single pixel inside the // cell but the cell-to-cell rhythm stays regular. // - // Ghostty uses 3 segments per cell (`special.zig:135`) + // uses 3 segments per cell // which meets DASH-to-DASH at every cell boundary — we // prefer the 4-segment layout because it stays periodic // under tiling. @@ -428,10 +717,10 @@ fn rasterize_decoration( DecorationStyle::CurlyUnderline => { // One arch per cell: baseline → peak-at-center → baseline, // with horizontal tangents at cell edges so tiled sprites - // join smoothly. Matches Ghostty's two-cubic-Bezier shape - // (`ghostty/src/font/sprite/draw/special.zig:167`): - // amplitude = cell_w / π - // stroke width = thickness, round caps + // join smoothly. two-cubic-Bezier shape + //: + // amplitude = cell_w / π + // stroke width = thickness, round caps // We approximate the Bezier with a raised cosine, which // has the same endpoints, same peak, and the same // horizontal tangent at the edges. The two curves differ @@ -528,7 +817,7 @@ fn underline_style_from_flags(flags: StyleFlags) -> Option { } /// Decoration color: SGR 58 `underline_color` if set, else the cell's -/// computed fg. Matches Ghostty `generic.zig:2968`. +/// computed fg. `generic.zig:2968`. #[inline] fn decoration_color( sq: Square, @@ -631,7 +920,7 @@ pub fn build_row_bg( let col = x as u16; let rgba = if cell_in_row_sel(row_sel, col) { // Selection bg wins over hint bg and the cell's own bg, - // matching Ghostty `generic.zig:2775-2800` (selection check + // matching `generic.zig:2775-2800` (selection check // runs before highlight check). sel_bg.unwrap_or_else(|| cell_bg(sq, style_set, renderer, term_colors)) } else if let Some(tag) = cell_in_row_hints(row_hints, col) { @@ -652,14 +941,14 @@ pub fn build_row_bg( } } -// Run-shaping infrastructure (platform-agnostic types) +// Run-shaping infrastructure (platform-agnostic types) /// Bits of `StyleFlags` that change shaping / font selection. Bold + /// italic pick different font files. Color / decoration / dim don't /// affect shaping so they don't break runs. const SHAPING_FLAG_MASK: u16 = StyleFlags::BOLD.bits() | StyleFlags::ITALIC.bits(); -/// 256 × 8 bucketed LRU cache — matches Ghostty's CellCacheTable. +/// 256 × 8 bucketed LRU cache — CellCacheTable. const RUN_BUCKET_COUNT: usize = 256; const RUN_BUCKET_SIZE: usize = 8; @@ -678,8 +967,8 @@ struct ShapedGlyph { struct RunCacheEntry { /// 64-bit rapidhash of (font_id, size_bucket, style_flags, run bytes). /// We key on the hash alone — no stored run string, no equality - /// check on lookup. Matches Ghostty's `CellCacheTable` pattern - /// (`font/shaper/Cache.zig:20-30`): rapidhash / wyhash pass + /// check on lookup. `CellCacheTable` pattern + ///: rapidhash / wyhash pass /// SMHasher, so a random collision costs a wrong-glyph frame /// until the next row rebuild but never corrupts state. Birthday /// bound at N=10k concurrent cache entries ≈ 2.7×10⁻¹². @@ -699,7 +988,7 @@ pub struct GridGlyphRasterizer { // macOS: stage the run in UTF-16 (what CoreText wants natively) // so the shaper call can hand the buffer straight to // `CFStringCreateWithCharactersNoCopy` with no encoding - // conversion. Matches Ghostty's `coretext.zig:88-104` — UTF-16 + // conversion. `coretext.zig:88-104` — UTF-16 // `unichars` + a parallel cell-start table for the cluster → // cell mapping. #[cfg(target_os = "macos")] @@ -773,7 +1062,7 @@ impl GridGlyphRasterizer { ) -> (u32, bool) { // ASCII printable + regular style → always primary font, never // emoji. Skips the FxHashMap lookup that dominates this fn's - // cost on terminal-typical content. Mirrors Ghostty's + // cost on terminal-typical content. // `font/Group.zig` indexForCodepoint ASCII fast path. // // Bold / italic ASCII still goes through the cache because @@ -831,7 +1120,7 @@ fn span_style_for_flags(style_flags: u8) -> rio_backend::sugarloaf::SpanStyle { } /// Rapidhash-based run key. Rapidhash is the official successor to -/// wyhash (Ghostty's choice at `font/shaper/run.zig:8`) — same +/// wyhash (choice) — same /// quality, passes SMHasher, near-ideal collision probability. We use /// the streaming `Hasher` API so we don't have to glue the inputs /// into a single byte slice. @@ -864,8 +1153,8 @@ fn is_run_breaker(sq: Square) -> bool { /// Lookup. Hash → bucket; scan from most-recent; rotate on hit. No /// secondary comparison — we trust the 64-bit rapidhash to be -/// collision-free across realistic workloads. Matches Ghostty -/// (`font/shaper/Cache.zig:27`). +/// collision-free across realistic workloads. Matches +///. fn run_cache_get( buckets: &mut [Vec], hash: u64, @@ -894,7 +1183,7 @@ fn run_cache_put(buckets: &mut [Vec], entry: RunCacheEntry) { bucket.push(entry); } -// Platform-specific shape + ascent helpers +// Platform-specific shape + ascent helpers /// Shape a single run on macOS via CoreText and populate /// `out.ascent_px` as a side effect via the rasterizer's cache. @@ -1000,13 +1289,13 @@ fn shape_run_swash( Some((glyphs, ascent_px)) } -// Emission +// Emission /// Run-level fg emission. Shapes once per run, emits one CellText per /// shaped glyph. Works on both macOS (CoreText) and non-macOS (swash). /// /// Emits in three ordered phases so decoration z-order matches -/// Ghostty's: underlines first (drawn under glyphs), glyphs, then +/// 's: underlines first (drawn under glyphs), glyphs, then /// strikethroughs (drawn on top). #[allow(clippy::too_many_arguments)] pub fn build_row_fg( @@ -1257,7 +1546,7 @@ pub fn build_row_fg( let src_sq = row[Column(src_col)]; let (atlas, color) = if is_color { // Colour glyphs (emoji) don't take the selection-fg / - // hint-fg swap — matches Ghostty's behaviour for + // hint-fg swap — behaviour for // bitmap/COLR atlas entries. (CellText::ATLAS_COLOR, [255, 255, 255, 255]) } else if !needs_per_cell_check { @@ -1281,7 +1570,7 @@ pub fn build_row_fg( ) } else if let Some(tag) = hint_tag { // Hint-fg wins over the cell's own fg, matching - // Ghostty's `.search` / `.search_selected` branches at + // `.search` / `.search_selected` branches at // `generic.zig:2829-2833` (the fg picker mirrors bg). (CellText::ATLAS_GRAYSCALE, cell_fg_hinted(tag, renderer)) } else { @@ -1350,7 +1639,7 @@ fn emit_underlines( // hover-only forced underline. When the cell has no SGR // decoration but is inside a hovered hyperlink, emit a plain // single-line underline using the cell fg color — same shape - // as Ghostty's hyperlink-hover affordance. + // as hyperlink-hover affordance. let (deco, hover_force) = match underline_style_from_flags(style.flags) { Some(d) => (d, false), None if cell_in_hover_underline(row_hints, col) => { @@ -1378,7 +1667,7 @@ fn emit_underlines( } else if hover_force { // Hover-only forced underline: use the cell fg so the // underline tracks the hyperlink text color (matches - // Ghostty's hyperlink hover affordance). + // hyperlink hover affordance). cell_fg(sq, style_set, renderer, term_colors) } else { decoration_color(sq, &style, style_set, renderer, term_colors) @@ -1432,7 +1721,7 @@ fn emit_strikethroughs( } let col = x as u16; // Strikethrough always uses the cell fg (there's no SGR for - // a separate strike color, matching Ghostty). + // a separate strike color, matching ). let color = if cell_in_row_sel(row_sel, col) { cell_fg_selected(sq, style_set, renderer, term_colors) } else if let Some(tag) = cell_in_row_hints(row_hints, col) { diff --git a/frontends/rioterm/src/hints.rs b/frontends/rioterm/src/hints.rs index dde53173..79bdd2d9 100644 --- a/frontends/rioterm/src/hints.rs +++ b/frontends/rioterm/src/hints.rs @@ -411,9 +411,9 @@ const URI_SCHEMES: &[&str] = &[ /// fall back to the raw text and let the OS opener handle it. /// /// Modelled on ghostty's `resolvePathForOpening` (`src/Surface.zig:2045`). -/// Ghostty's core only joins relative paths against the OSC 7 cwd; tilde +/// core only joins relative paths against the OSC 7 cwd; tilde /// expansion lives in the macOS apprt's Swift `openURL` -/// (`Ghostty.App.swift:715`, via `NSString.standardizingPath`), so `~/x` +/// (`.App.swift:715`, via `NSString.standardizingPath`), so `~/x` /// works on macOS but isn't expanded on Linux/BSD where `xdg-open` gets the /// literal `~`. Rio doesn't have a per-platform apprt layer, so we do the /// expansion here to get consistent cross-platform behaviour: @@ -424,7 +424,7 @@ const URI_SCHEMES: &[&str] = &[ /// 3. Strings starting with a known URI scheme are rejected up front so the /// OS opener routes them as URLs (saves one filesystem syscall vs /// ghostty's "join cwd + stat → fail" path). -/// 4. Absolute paths are existence-checked too. Ghostty short-circuits +/// 4. Absolute paths are existence-checked too. short-circuits /// absolute paths to `None` (caller passes raw); user-visible behaviour /// is the same since the raw and resolved strings match. pub fn resolve_path_for_opening(text: &str, cwd: Option<&Path>) -> Option { diff --git a/frontends/rioterm/src/layout/mod.rs b/frontends/rioterm/src/layout/mod.rs index 2be55da4..66ab0bbf 100644 --- a/frontends/rioterm/src/layout/mod.rs +++ b/frontends/rioterm/src/layout/mod.rs @@ -66,14 +66,14 @@ fn compute( return (MIN_COLS, MIN_LINES); } - // Calculate columns - divide by the ROUNDED cell width. Ghostty + // Calculate columns - divide by the ROUNDED cell width. // rounds `face_width` once in `font/Metrics.zig:265` (`cell_width = // @round(face_width)`) and uses that integer everywhere — cols, // grid shader, cursor hit-testing. Rio's grid renderer already // does `.round()` on `cell_w` when building `GridUniforms`, so the // column count has to use the same integer or the right edge of // the grid floats `cols * (face_width - cell_width)` pixels short - // of the panel. Matches Ghostty; sacrifices at most 1 col vs + // of the panel. Matches ; sacrifices at most 1 col vs // fractional divide but keeps the render perfectly aligned. let cell_width = dimensions.width.round().max(1.0); let visible_columns = diff --git a/frontends/rioterm/src/mouse/mod.rs b/frontends/rioterm/src/mouse/mod.rs index 7d8c0382..efc55eaa 100644 --- a/frontends/rioterm/src/mouse/mod.rs +++ b/frontends/rioterm/src/mouse/mod.rs @@ -512,12 +512,12 @@ pub mod test { /// Regression: margins passed to calculate_mouse_position are already /// pre-scaled (multiplied by scale_factor in update_scaled_margin), but the - /// function multiplied them by scale_factor again. With a 2× display and + /// function multiplied them by scale_factor again. With a 2× display and /// margin_y_top=72 (already 36*2), the double-scaling produces 144 instead /// of 72, shifting the row calculation by ~2 rows. /// /// Uses exact values observed on a Retina display: - /// cell 16.41×33, margin (4, 72) pre-scaled, scale 2.0, 96×27 grid. + /// cell 16.41×33, margin (4, 72) pre-scaled, scale 2.0, 96×27 grid. #[test] fn test_row_not_double_scaled() { let display_offset = 0; @@ -546,7 +546,7 @@ pub mod test { (cell_w, cell_h), ); // (280 - 72) / 33 = 6.3 → row 6 - // Bug: (280 - 144) / 33 = 4.1 → row 4 (margin double-scaled) + // Bug: (280 - 144) / 33 = 4.1 → row 4 (margin double-scaled) assert_eq!(pos.row, Line(6)); // Row 22 spans y = [72 + 22*33, 72 + 23*33) = [798, 831). @@ -569,7 +569,7 @@ pub mod test { } /// Same double-scaling issue on the X axis, but less visible with small - /// margins. With margin_x_left=20 (pre-scaled) and scale=2.0 the error + /// margins. With margin_x_left=20 (pre-scaled) and scale=2.0 the error /// is 20 extra pixels — enough to shift a column. #[test] fn test_col_not_double_scaled() { @@ -597,7 +597,7 @@ pub mod test { (cell_w, cell_h), ); // (70 - 20) / 16.41 = 3.05 → col 3 - // Bug: (70 - 40) / 16 = 1.87 → col 1 (margin double-scaled + int truncation) + // Bug: (70 - 40) / 16 = 1.87 → col 1 (margin double-scaled + int truncation) assert_eq!(pos.col, Column(3)); } diff --git a/frontends/rioterm/src/renderer/island.rs b/frontends/rioterm/src/renderer/island.rs index a712f91e..11485362 100644 --- a/frontends/rioterm/src/renderer/island.rs +++ b/frontends/rioterm/src/renderer/island.rs @@ -1019,9 +1019,9 @@ mod tests { fn title_respects_budget_with_wide_chars() { // Mixed widths: 'W' = 2.0, others (including ellipsis) = 1.0. // Title "WxWxW", budget 4.0. Walk: - // ix=0 W: before add, 0+1(suffix) ≤ 4 → truncate_ix=0; accum→2 - // ix=1 x: 2+1 ≤ 4 → truncate_ix=1; accum→3 - // ix=2 W: 3+1 ≤ 4 → truncate_ix=2; accum→5; 5>4 → cut. + // ix=0 W: before add, 0+1(suffix) ≤ 4 → truncate_ix=0; accum→2 + // ix=1 x: 2+1 ≤ 4 → truncate_ix=1; accum→3 + // ix=2 W: 3+1 ≤ 4 → truncate_ix=2; accum→5; 5>4 → cut. // Output: title[..2] + "…" = "Wx…", width 2+1+1 = 4 ≤ 4 ✓ let widths = |c: char| if c == 'W' { 2.0 } else { 1.0 }; let out = fit_title_with_widths("WxWxW", 4.0, widths); diff --git a/frontends/rioterm/src/renderer/mod.rs b/frontends/rioterm/src/renderer/mod.rs index d7ca0551..9d1a15cd 100644 --- a/frontends/rioterm/src/renderer/mod.rs +++ b/frontends/rioterm/src/renderer/mod.rs @@ -376,7 +376,7 @@ impl Renderer { // Recalculate image overlay positions every frame when placements // exist. Positions depend on display_offset and history_size which - // change on scroll and text output (like Ghostty's approach). + // change on scroll and text output (like approach). let has_overlays = !terminal_snapshot.kitty_placements.is_empty(); let has_virtual = !terminal_snapshot.kitty_virtual_placements.is_empty(); if has_overlays || has_virtual { @@ -401,7 +401,7 @@ impl Renderer { for p in &terminal_snapshot.kitty_placements { let screen_row = p.dest_row - (history_size - display_offset); let image_bottom_row = screen_row + p.rows as i64; - // Cull only if fully off-screen (like Ghostty) + // Cull only if fully off-screen (like ) if image_bottom_row <= 0 || screen_row >= screen_lines { continue; } @@ -677,7 +677,7 @@ impl Renderer { // every frame, not just the frame where OSC arrived. Without // this, switching from a panel that ran OSC 11 to one that // didn't keeps sugarloaf's bg stuck at the OSC color — we - // want it to follow focus the way Ghostty does (each surface's + // want it to follow focus the way does (each surface's // `terminal.colors.background` drives its own window chrome). let current_context = context_manager.current_grid_mut().current_mut(); let effective_bg = match ¤t_context.renderable_content.background { @@ -738,18 +738,18 @@ impl Renderer { /// push one `GraphicOverlay` per row-run. Ports the four key behaviors /// from ghostty's `graphics_unicode.zig`: /// - /// 1. Per-row `kitty_virtual_placeholder` flag check skips rows - /// with no placeholders (`page.zig:1953-1958`). - /// 2. Continuation rules — a cell with missing diacritics inherits - /// from the previous cell on the row (`canAppend`, - /// `graphics_unicode.zig:506-513`). - /// 3. Run aggregation — consecutive cells with same image / row / - /// sequential column collapse into one Placement - /// (`PlacementIterator.next`, `graphics_unicode.zig:36-99`). - /// 4. Per-run source rect with aspect-fit + centering — handles - /// partial visibility (placement scrolled half off-screen) and - /// cells that fall in the centering padding - /// (`renderPlacement`, `graphics_unicode.zig:212-329`). + /// 1. Per-row `kitty_virtual_placeholder` flag check skips rows + /// with no placeholders. + /// 2. Continuation rules — a cell with missing diacritics inherits + /// from the previous cell on the row (`canAppend`, + /// `graphics_unicode.zig:506-513`). + /// 3. Run aggregation — consecutive cells with same image / row / + /// sequential column collapse into one Placement + /// (`PlacementIterator.next`, `graphics_unicode.zig:36-99`). + /// 4. Per-run source rect with aspect-fit + centering — handles + /// partial visibility (placement scrolled half off-screen) and + /// cells that fall in the centering padding + /// (`renderPlacement`, `graphics_unicode.zig:212-329`). fn push_virtual_placeholder_overlays( overlays: &mut Vec, snapshot: &TerminalSnapshot, diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs index 2d73cd73..60bf8ada 100644 --- a/frontends/rioterm/src/screen/mod.rs +++ b/frontends/rioterm/src/screen/mod.rs @@ -899,7 +899,7 @@ impl Screen<'_> { // // For more see https://github.com/rust-windowing/winit/issues/2945. // if (cfg!(target_os = "macos") || (cfg!(windows) && mods.control_key())) - // && mods.alt_key() + // && mods.alt_key() if (mods.shift_key() || mods.alt_key()) || mods.alt_key() && (cfg!(windows) && mods.control_key()) { @@ -1893,11 +1893,11 @@ impl Screen<'_> { // Mark the hint range as damaged so it gets re-rendered. // // Two damage signals are required: - // * Terminal-side: `update_selection_damage` marks the affected - // lines so the partial render path knows what to redraw. - // * Renderer-side: `pending_update.set_terminal_damage(Full)` - // ensures the render loop doesn't early-exit on - // `!pending_update.is_dirty()` + // * Terminal-side: `update_selection_damage` marks the affected + // lines so the partial render path knows what to redraw. + // * Renderer-side: `pending_update.set_terminal_damage(Full)` + // ensures the render loop doesn't early-exit on + // `!pending_update.is_dirty()` { let mut terminal = current.terminal.lock(); let display_offset = terminal.display_offset(); @@ -2836,7 +2836,7 @@ impl Screen<'_> { // Force unlimited search if the previous one was interrupted. // let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id()); // if self.scheduler.scheduled(timer_id) { - // self.goto_match(None); + // self.goto_match(None); // } self.exit_search(); @@ -3521,17 +3521,17 @@ impl Screen<'_> { // Phase 2.2/2.3: per-panel CellBg + CellText emission with // per-row dirty gating. Iterates every panel in the active // grid. For each: - // - `damage == Noop | CursorOnly` + grid not forcing full: - // skip `write_row` entirely. Cursor state is carried - // by `GridUniforms`, so a pure blink/move doesn't - // touch the cell buffers. - // - `damage == Full` | first-frame | resize: - // rebuild every visible row. - // - `damage == Partial(lines)`: - // rebuild only those rows. + // - `damage == Noop | CursorOnly` + grid not forcing full: + // skip `write_row` entirely. Cursor state is carried + // by `GridUniforms`, so a pure blink/move doesn't + // touch the cell buffers. + // - `damage == Full` | first-frame | resize: + // rebuild every visible row. + // - `damage == Partial(lines)`: + // rebuild only those rows. // Unchanged rows keep their CellBg + CellText resident in // the grid's CPU state, which is re-uploaded verbatim. Same - // pattern as Ghostty's `.partial` path at + // pattern as `.partial` path at // `ghostty/src/renderer/generic.zig:2431-2440`. { struct PanelFrame { @@ -3552,6 +3552,29 @@ impl Screen<'_> { cursor_col: u16, cursor_row: u16, cursor_visible: bool, + /// Terminal-side cursor shape (block / underline / + /// beam / hidden). Driven by DECSCUSR + the + /// configured default. Mapped to a render style + /// inside the rebuild loop. + cursor_shape: rio_backend::ansi::CursorShape, + /// `true` when the terminal has cursor blink + /// enabled (DECTCEM blink mode or SGR cursor blink). + cursor_blinking: bool, + /// `true` for the visible half of the blink cycle. + /// Always `true` when blink isn't enabled. Driven + /// by `Renderer::run`'s blink toggler. + cursor_blink_visible: bool, + /// `true` while an IME pre-edit string is active — + /// forces a block cursor regardless of the + /// configured shape so the user can tell IME is + /// taking input. + cursor_preedit: bool, + /// Resolved cursor color: OSC 12 wins, then config / + /// theme `cursor`. + /// `state.colors.cursor → config.cursor_color` + /// resolution. Per-panel + /// because each terminal can issue its own OSC 12. + cursor_color: rio_backend::config::colors::ColorArray, is_active: bool, damage: rio_backend::event::TerminalDamage, /// Selection is per-context (`renderable_content`), not @@ -3566,12 +3589,12 @@ impl Screen<'_> { /// search is inactive. Consumed alongside `selection` /// inside `build_row_bg` / `build_row_fg` to apply /// `search_match_background` / `_foreground`. Mirrors - /// Ghostty's `row_data.highlights` at + /// `row_data.highlights` at /// `ghostty/src/renderer/generic.zig:1317`. hint_matches: Option>, /// Currently-focused search match (↑/↓ navigation). /// Rendered with `search_focused_match_background` / - /// `_foreground` — matches Ghostty's `.search_selected` + /// `_foreground` — `.search_selected` /// highlight tag. focused_match: Option, /// (start, end) of the currently-hovered hyperlink / @@ -3664,6 +3687,19 @@ impl Screen<'_> { } else { None }; + let cursor_shape = cursor.state.content; + let cursor_blinking = ctx.renderable_content.has_blinking_enabled; + let cursor_blink_visible = + !cursor_blinking || ctx.renderable_content.is_blinking_cursor_visible; + let cursor_preedit = ctx.ime.preedit().is_some(); + // OSC 12 wins; otherwise fall back to the named-color + // theme value. `Renderer::color`'s fallback (the + // indexed-color List) is not populated for the Cursor + // slot — `List::fill_named` skips it — so we read + // `named_colors.cursor` directly. + let cursor_color = term_colors + [rio_backend::config::colors::NamedColor::Cursor as usize] + .unwrap_or(self.renderer.named_colors.cursor); panels.push(PanelFrame { route_id: ctx.route_id, layout_rect: item.layout_rect, @@ -3678,6 +3714,11 @@ impl Screen<'_> { cursor_col: cursor.state.pos.col.0 as u16, cursor_row: cursor.state.pos.row.0 as u16, cursor_visible: cursor.state.is_visible(), + cursor_shape, + cursor_blinking, + cursor_blink_visible, + cursor_preedit, + cursor_color, is_active, damage, selection, @@ -3701,9 +3742,8 @@ impl Screen<'_> { // feeds into `Globals` — the grid shader applies the // matching sRGB → DisplayP3 transform so cell bg, window // fill, and UI overlays produce identical framebuffer - // colors. Matches Ghostty's single `load_color` path. + // colors. single `load_color` path. let input_colorspace = self.sugarloaf.input_colorspace(); - let cursor_col_rgba = self.renderer.named_colors.cursor; let mut frame_grids: Vec<( &mut rio_backend::sugarloaf::grid::GridRenderer, @@ -3720,10 +3760,10 @@ impl Screen<'_> { // Decide which rows to rebuild. // // `force_full` short-circuits damage to "rebuild all": - // - grid was just created or resized (CPU buffers - // are zeroed, so whatever damage says we have to - // do a full fill). - // - damage == Full (the terminal explicitly asked). + // - grid was just created or resized (CPU buffers + // are zeroed, so whatever damage says we have to + // do a full fill). + // - damage == Full (the terminal explicitly asked). // // `Noop` / `CursorOnly` → no row rebuilds, uniforms // alone carry the frame's state change. @@ -3845,12 +3885,56 @@ impl Screen<'_> { } } + // Cursor pipeline (`addCursor` / + // `cursor.style()`): + // 1. Decide render style with strict priority: + // preedit > visible > focused > blink > shape. + // 2. Always clear both cursor slots first — last + // frame's sprite (if any) needs to disappear + // whether we emit a new one or not. + // 3. Some(style): emit a sprite into slot 0 (block) + // or slot rows+1 (others). For Block we ALSO + // write the bg-tint uniforms below so the bg + // fragment paints the block + the text shader + // inverts the underlying glyph. + // 4. None: leave both slots empty + zero uniforms. + let render_style = crate::grid_emit::cursor_render_style( + crate::grid_emit::CursorRenderInputs { + visible: p.cursor_visible, + focused: p.is_active, + blink_visible: p.cursor_blink_visible, + blinking: p.cursor_blinking, + preedit: p.cursor_preedit, + shape: p.cursor_shape, + }, + ); + grid.clear_cursor(); + if let Some(style) = render_style { + let cell_w = p.cell_w.round().clamp(1.0, u32::MAX as f32) as u32; + let cell_h = p.cell_h.round().clamp(1.0, u32::MAX as f32) as u32; + let cursor_color = [ + (p.cursor_color[0].clamp(0.0, 1.0) * 255.0) as u8, + (p.cursor_color[1].clamp(0.0, 1.0) * 255.0) as u8, + (p.cursor_color[2].clamp(0.0, 1.0) * 255.0) as u8, + 255, + ]; + crate::grid_emit::emit_cursor_sprite( + grid, + style, + p.cursor_col, + p.cursor_row, + cursor_color, + cell_w, + cell_h, + ); + } + // Panel's grid origin in drawable-pixel space = // window scaled_margin + the panel's layout rect // offset inside the root container. Snap to integer // pixels so `cell_size * grid_pos + grid_padding` // always lands on pixel boundaries — same approach - // as Ghostty's `@floatFromInt(blank.top)` at + // as `@floatFromInt(blank.top)` at // `ghostty/src/renderer/generic.zig:1976-1981`. // Without this, a fractional margin (e.g. Taffy // layout computing 10.5px offsets) shifts the whole @@ -3861,21 +3945,26 @@ impl Screen<'_> { let panel_left = (scaled_margin.left + p.layout_rect[0]).round(); let panel_top = (scaled_margin.top + p.layout_rect[1]).round(); - let (cursor_pos, cursor_col_u, cursor_bg_u) = - if p.is_active && p.cursor_visible { - ( - [p.cursor_col as u32, p.cursor_row as u32], - [bg_col[0], bg_col[1], bg_col[2], bg_col[3]], - [ - cursor_col_rgba[0], - cursor_col_rgba[1], - cursor_col_rgba[2], - 1.0, - ], - ) - } else { - ([u32::MAX; 2], [0.0; 4], [0.0; 4]) - }; + // Bg-tint uniforms fire ONLY for the active block + // style — the bg shader paints the cursor cell in + // `cursor_bg_color` and the text shader swaps glyph + // fg to `cursor_color` (so the character inverts on + // top of the block). All other styles (bar / + // underline / hollow) draw via the sprite emitted + // above; their bg/text stays untouched. Same gate as + // . + let (cursor_pos, cursor_col_u, cursor_bg_u) = if matches!( + render_style, + Some(crate::grid_emit::CursorRenderStyle::Block) + ) { + ( + [p.cursor_col as u32, p.cursor_row as u32], + [bg_col[0], bg_col[1], bg_col[2], bg_col[3]], + [p.cursor_color[0], p.cursor_color[1], p.cursor_color[2], 1.0], + ) + } else { + ([u32::MAX; 2], [0.0; 4], [0.0; 4]) + }; let uniforms = rio_backend::sugarloaf::grid::GridUniforms { projection: diff --git a/rio-backend/src/ansi/kitty_graphics_protocol.rs b/rio-backend/src/ansi/kitty_graphics_protocol.rs index 5c4cc515..66f9a595 100644 --- a/rio-backend/src/ansi/kitty_graphics_protocol.rs +++ b/rio-backend/src/ansi/kitty_graphics_protocol.rs @@ -453,9 +453,9 @@ pub fn parse( // Determine the key for this chunk: // - If this chunk has an explicit image_id or image_number, use that. // - If no ID in this chunk and we are mid-transmission (a chunked - // command pinned `current_transmission_key`), reuse it. + // command pinned `current_transmission_key`), reuse it. // - Otherwise the client sent a fresh command without an explicit id - // and we must allocate one per kitty spec. + // and we must allocate one per kitty spec. // // Importantly we only *pin* the key into `current_transmission_key` // when this is a chunked command (`cmd.more` is true). Pinning on @@ -1792,9 +1792,9 @@ mod tests { // of this test is the chunking pattern, not meaningful pixels. // // Chunk 1: encodes 4 bytes [0xDE, 0xAD, 0xBE, 0xEF] - // → 8 chars with padding: "3q2+7w==" + // → 8 chars with padding: "3q2+7w==" // Chunk 2: encodes 3 bytes [0xCA, 0xFE, 0xBA] - // → 4 chars no padding: "yv66" + // → 4 chars no padding: "yv66" // // Concatenated raw base64 would be "3q2+7w==yv66" with `==` in // the middle — which a strict base64 decoder rejects. @@ -2264,7 +2264,7 @@ mod tests { let response = result.expect("response struct must exist"); assert!(response.response.is_none(), "q=1 must suppress OK"); - // q=2 suppresses everything, including errors. Ghostty / kitty + // q=2 suppresses everything, including errors. / kitty // both document this as "absolute silence". let result = parse_kitty_graphics_protocol("a=q,i=1,q=2", ""); let response = result.expect("response struct must exist"); diff --git a/rio-backend/src/ansi/kitty_virtual.rs b/rio-backend/src/ansi/kitty_virtual.rs index 9fc2b39f..1d57be62 100644 --- a/rio-backend/src/ansi/kitty_virtual.rs +++ b/rio-backend/src/ansi/kitty_virtual.rs @@ -419,7 +419,7 @@ fn color_to_id(color: AnsiColor) -> u32 { /// Per-cell decode of a U+10EEEE placeholder, before continuation rules /// resolve missing diacritics. Mirrors ghostty's `IncompletePlacement` -/// (`graphics_unicode.zig:407-494`): row / col / `image_id_high` are +///: row / col / `image_id_high` are /// `Option` because kitty allows applications to omit later diacritics /// when they would inherit from the previous cell. /// @@ -465,12 +465,12 @@ impl IncompletePlacement { } /// True if `other` (the next cell in the same row) can extend this - /// run. Mirrors ghostty's `canAppend` (`graphics_unicode.zig:506-513`): - /// - same `image_id_low` and `placement_id` - /// - `other.row` is missing (inherit) or matches `self.row` - /// - `other.col` is missing (inherit + auto-increment) or equals - /// `self.col + self.width` (sequential) - /// - `other.image_id_high` is missing or matches + /// run. Mirrors ghostty's `canAppend`: + /// - same `image_id_low` and `placement_id` + /// - `other.row` is missing (inherit) or matches `self.row` + /// - `other.col` is missing (inherit + auto-increment) or equals + /// `self.col + self.width` (sequential) + /// - `other.image_id_high` is missing or matches pub fn can_append(&self, other: &IncompletePlacement) -> bool { self.image_id_low == other.image_id_low && self.placement_id == other.placement_id @@ -489,7 +489,7 @@ impl IncompletePlacement { } /// Resolve the run into a final placement, defaulting any still-`None` - /// fields. Mirrors ghostty's `complete()` (`graphics_unicode.zig:520-535`). + /// fields. Mirrors ghostty's `complete()`. pub fn complete(&self) -> PlaceholderRun { PlaceholderRun { image_id: ((self.image_id_high.unwrap_or(0) as u32) << 24) @@ -535,7 +535,7 @@ pub struct RunGeometry { /// Compute the screen rect + source rect for one row-run, taking the /// placement's grid size, the image's pixel size, and the cell metrics. /// Mirrors ghostty's `Placement.renderPlacement` -/// (`graphics_unicode.zig:130-351`), specialised for a single row of +///, specialised for a single row of /// cells (height = 1 cell). Returns `None` if the run lies entirely in /// the centering padding (no pixels to draw). /// diff --git a/rio-backend/src/config/hints.rs b/rio-backend/src/config/hints.rs index 96ead5fb..f555bf87 100644 --- a/rio-backend/src/config/hints.rs +++ b/rio-backend/src/config/hints.rs @@ -20,7 +20,7 @@ pub const DEFAULT_HINTS_ALPHABET: &str = "jfkdls;ahgurieowpq"; /// 3. **Bare relative paths** — `word/.../name.ext`. A dotted segment is /// required, and lookbehinds prevent matching mid-word starts. pub const DEFAULT_URL_REGEX: &str = concat!( - // schemed URLs + // schemed URLs "(?:https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)", "(?:", r"(?:\[[:0-9a-fA-F]+(?:[:0-9a-fA-F]*)+\](?::[0-9]+)?)", @@ -29,17 +29,17 @@ pub const DEFAULT_URL_REGEX: &str = concat!( ")+", r"(? Crosswords { self.damage_cursor_line(); // self.event_proxy.send_event( - // RioEvent::TerminalDamaged { - // route_id: self.route_id, - // damage: TerminalDamage::CursorOnly(self.grid.cursor.pos.line, None), - // }, - // self.window_id, + // RioEvent::TerminalDamaged { + // route_id: self.route_id, + // damage: TerminalDamage::CursorOnly(self.grid.cursor.pos.line, None), + // }, + // self.window_id, // ); } @@ -999,11 +999,11 @@ impl Crosswords { self.damage_cursor_line(); // self.event_proxy.send_event( - // RioEvent::TerminalDamaged { - // route_id: self.route_id, - // damage: TerminalDamage::CursorOnly, - // }, - // self.window_id, + // RioEvent::TerminalDamaged { + // route_id: self.route_id, + // damage: TerminalDamage::CursorOnly, + // }, + // self.window_id, // ); } } @@ -1606,9 +1606,9 @@ impl Crosswords { /// Append cells from a single line to `text`, buffering blank cells /// (`\0` and trailing spaces) so that: - /// - `\0` cells inside a run of content become real spaces - /// - trailing blanks at end of the run are dropped (caller decides - /// whether to flush them via the `blank_cells` accumulator) + /// - `\0` cells inside a run of content become real spaces + /// - trailing blanks at end of the run are dropped (caller decides + /// whether to flush them via the `blank_cells` accumulator) /// /// Returns true if the line emitted any non-blank content. fn append_cells( @@ -3206,32 +3206,32 @@ impl Handler for Crosswords { fn graphics_attribute(&mut self, pi: u16, pa: u16) { // From Xterm documentation: // - // CSI ? Pi ; Pa ; Pv S + // CSI ? Pi ; Pa ; Pv S // - // Pi = 1 -> item is number of color registers. - // Pi = 2 -> item is Sixel graphics geometry (in pixels). - // Pi = 3 -> item is ReGIS graphics geometry (in pixels). + // Pi = 1 -> item is number of color registers. + // Pi = 2 -> item is Sixel graphics geometry (in pixels). + // Pi = 3 -> item is ReGIS graphics geometry (in pixels). // - // Pa = 1 -> read attribute. - // Pa = 2 -> reset to default. - // Pa = 3 -> set to value in Pv. - // Pa = 4 -> read the maximum allowed value. + // Pa = 1 -> read attribute. + // Pa = 2 -> reset to default. + // Pa = 3 -> set to value in Pv. + // Pa = 4 -> read the maximum allowed value. // - // Pv is ignored by xterm except when setting (Pa == 3). - // Pv = n <- A single integer is used for color registers. - // Pv = width ; height <- Two integers for graphics geometry. + // Pv is ignored by xterm except when setting (Pa == 3). + // Pv = n <- A single integer is used for color registers. + // Pv = width ; height <- Two integers for graphics geometry. // - // xterm replies with a control sequence of the same form: + // xterm replies with a control sequence of the same form: // - // CSI ? Pi ; Ps ; Pv S + // CSI ? Pi ; Ps ; Pv S // - // where Ps is the status: - // Ps = 0 <- success. - // Ps = 1 <- error in Pi. - // Ps = 2 <- error in Pa. - // Ps = 3 <- failure. + // where Ps is the status: + // Ps = 0 <- success. + // Ps = 1 <- error in Pi. + // Ps = 2 <- error in Pa. + // Ps = 3 <- failure. // - // On success, Pv represents the value read or set. + // On success, Pv represents the value read or set. fn generate_response(pi: u16, ps: u16, pv: &[usize]) -> String { use std::fmt::Write; @@ -3510,7 +3510,7 @@ impl Handler for Crosswords { // Bg-only cells (BgPalette/BgRgb) reuse the upper 32 // bits for the background color — `extras_id()` and - // `set_extras_id()` would read/write garbage. Reset + // `set_extras_id()` would read/write garbage. Reset // to a plain Codepoint cell so the extras slot is // usable. if cell_ref.is_bg_only() { @@ -5021,14 +5021,14 @@ mod tests { fn trailing_space_carries_across_wrap_continuation() { let mut term = make_term_for_selection(2, 5); let grid = &mut term.grid; - // Row 0: "ab " with a wrap into row 1. + // Row 0: "ab " with a wrap into row 1. grid[Line(0)][Column(0)].set_c('a'); grid[Line(0)][Column(1)].set_c('b'); grid[Line(0)][Column(2)].set_c(' '); grid[Line(0)][Column(3)].set_c(' '); grid[Line(0)][Column(4)].set_c(' '); grid[Line(0)][Column(4)].set_wrapline(true); - // Row 1: " cd " + // Row 1: " cd " grid[Line(1)][Column(0)].set_c(' '); grid[Line(1)][Column(1)].set_c('c'); grid[Line(1)][Column(2)].set_c('d'); @@ -6026,9 +6026,9 @@ mod tests { let b = image_id & 0xFF; // 1) Transmit a 1×1 RGBA pixel under the chosen image_id (we - // don't care about the pixel data — we just need an entry in - // `kitty_images` so the renderer's existence check passes). - // base64("\xFF\x00\x00\xFF") = "/wAA/w==". + // don't care about the pixel data — we just need an entry in + // `kitty_images` so the renderer's existence check passes). + // base64("\xFF\x00\x00\xFF") = "/wAA/w==". let xmit = format!("\x1b_Gf=32,a=t,i={image_id},s=1,v=1;/wAA/w==\x1b\\"); processor.advance(&mut cw, xmit.as_bytes()); @@ -6039,8 +6039,8 @@ mod tests { processor.advance(&mut cw, place.as_bytes()); // 3) Emit the placeholder cells themselves (what icat writes - // after the placement APC). Set fg via colon-separated SGR, - // write `` per cell. + // after the placement APC). Set fg via colon-separated SGR, + // write `` per cell. let id_high_diac = DIACRITICS[high as usize]; let mut cells = format!("\x1b[38:2:{r}:{g}:{b}m"); for (row, &row_diac) in DIACRITICS.iter().enumerate().take(rows as usize) { diff --git a/rio-backend/src/crosswords/square.rs b/rio-backend/src/crosswords/square.rs index 33017238..049ceda4 100644 --- a/rio-backend/src/crosswords/square.rs +++ b/rio-backend/src/crosswords/square.rs @@ -20,22 +20,22 @@ use std::sync::Arc; // --------------------------------------------------------------------------- // Bit layout for Square(u64) // -// bits 0..20 (21): codepoint (Unicode scalar value, max 0x10_FFFF) -// OR low bits of bg color when content_tag != Codepoint -// bits 21..22 (2): wide (Wide enum) -// bits 23..29 (7): per-cell flag bits (CellFlags), incl WRAPLINE at bit 0 -// bits 30..31 (2): content_tag (NEW) -// 0 = Codepoint (text cell, use style_id below) -// 1 = BgPalette (bg-only cell, palette index in 32..39) -// 2 = BgRgb (bg-only cell, RGB packed in 32..55) -// 3 = reserved -// bits 32..47 (16): style_id (when tag == Codepoint) -// bg palette idx in low 8 (when tag == BgPalette) -// bg RGB.r:g in low 16 (when tag == BgRgb) -// bits 48..63 (16): extras_id (when tag == Codepoint) -// bg RGB.b in low 8 (when tag == BgRgb) +// bits 0..20 (21): codepoint (Unicode scalar value, max 0x10_FFFF) +// OR low bits of bg color when content_tag != Codepoint +// bits 21..22 (2): wide (Wide enum) +// bits 23..29 (7): per-cell flag bits (CellFlags), incl WRAPLINE at bit 0 +// bits 30..31 (2): content_tag (NEW) +// 0 = Codepoint (text cell, use style_id below) +// 1 = BgPalette (bg-only cell, palette index in 32..39) +// 2 = BgRgb (bg-only cell, RGB packed in 32..55) +// 3 = reserved +// bits 32..47 (16): style_id (when tag == Codepoint) +// bg palette idx in low 8 (when tag == BgPalette) +// bg RGB.r:g in low 16 (when tag == BgRgb) +// bits 48..63 (16): extras_id (when tag == Codepoint) +// bg RGB.b in low 8 (when tag == BgRgb) // -// The bg-only encoding (BgPalette / BgRgb) is the Ghostty trick: cells that +// The bg-only encoding (BgPalette / BgRgb) is the trick: cells that // represent a colored background with no text don't need a style table // lookup at all, which is the dominant cost for large filled regions // (selection, padding, blank lines after `clear`, color blocks). @@ -127,18 +127,18 @@ impl ContentTag { } bitflags! { - /// Per-cell flags that DON'T live in the style table. SGR-related - /// attributes (bold, italic, underline, etc.) live in `StyleFlags`. + /// Per-cell flags that DON'T live in the style table. SGR-related + /// attributes (bold, italic, underline, etc.) live in `StyleFlags`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct CellFlags: u8 { - /// Soft-wrap continuation marker on the last cell of a wrapped line. + /// Soft-wrap continuation marker on the last cell of a wrapped line. const WRAPLINE = 1 << 0; - /// Cell carries graphics data (sixel / iTerm2 inline image piece). - /// Look up the actual graphic in `Grid::extras_table` via `extras_id`. + /// Cell carries graphics data (sixel / iTerm2 inline image piece). + /// Look up the actual graphic in `Grid::extras_table` via `extras_id`. const GRAPHICS = 1 << 1; - /// Cell carries hyperlink metadata. Lookup via extras_id. + /// Cell carries hyperlink metadata. Lookup via extras_id. const HYPERLINK = 1 << 2; - /// Cell carries multi-codepoint grapheme cluster. Lookup via extras_id. + /// Cell carries multi-codepoint grapheme cluster. Lookup via extras_id. const GRAPHEME = 1 << 3; } } diff --git a/rio-window/src/platform_impl/macos/app_delegate.rs b/rio-window/src/platform_impl/macos/app_delegate.rs index 8b319220..3e8858b6 100644 --- a/rio-window/src/platform_impl/macos/app_delegate.rs +++ b/rio-window/src/platform_impl/macos/app_delegate.rs @@ -188,8 +188,8 @@ declare_class!( false } - // NOTE: This will, globally, only be run once, no matter how many - // `EventLoop`s the user creates. + // NOTE: This will, globally, only be run once, no matter how many + // `EventLoop`s the user creates. #[method(applicationDidFinishLaunching:)] fn did_finish_launching(&self, _sender: Option<&AnyObject>) { trace_scope!("applicationDidFinishLaunching:"); @@ -197,17 +197,17 @@ declare_class!( let mtm = MainThreadMarker::from(self); let app = NSApplication::sharedApplication(mtm); - // We need to delay setting the activation policy and activating the app - // until `applicationDidFinishLaunching` has been called. Otherwise the - // menu bar is initially unresponsive on macOS 10.15. + // We need to delay setting the activation policy and activating the app + // until `applicationDidFinishLaunching` has been called. Otherwise the + // menu bar is initially unresponsive on macOS 10.15. app.setActivationPolicy(self.ivars().activation_policy.0); #[allow(deprecated)] app.activateIgnoringOtherApps(self.ivars().activate_ignoring_other_apps); if self.ivars().default_menu { - // The menubar initialization should be before the `NewEvents` event, to allow - // overriding of the default menu even if it's created + // The menubar initialization should be before the `NewEvents` event, to allow + // overriding of the default menu even if it's created menu::initialize(&app); } @@ -216,17 +216,17 @@ declare_class!( self.set_is_running(true); self.dispatch_init_events(); - // If the application is being launched via `EventLoop::pump_app_events()` then we'll - // want to stop the app once it is launched (and return to the external loop) - // - // In this case we still want to consider Winit's `EventLoop` to be "running", - // so we call `start_running()` above. + // If the application is being launched via `EventLoop::pump_app_events()` then we'll + // want to stop the app once it is launched (and return to the external loop) + // + // In this case we still want to consider Winit's `EventLoop` to be "running", + // so we call `start_running()` above. if self.ivars().stop_on_launch.get() { - // NOTE: the original idea had been to only stop the underlying `RunLoop` - // for the app but that didn't work as expected (`-[NSApplication run]` - // effectively ignored the attempt to stop the RunLoop and re-started it). - // - // So we return from `pump_events` by stopping the application. + // NOTE: the original idea had been to only stop the underlying `RunLoop` + // for the app but that didn't work as expected (`-[NSApplication run]` + // effectively ignored the attempt to stop the RunLoop and re-started it). + // + // So we return from `pump_events` by stopping the application. let app = NSApplication::sharedApplication(mtm); stop_app_immediately(&app); } @@ -235,7 +235,7 @@ declare_class!( #[method(applicationWillTerminate:)] fn will_terminate(&self, _sender: Option<&AnyObject>) { trace_scope!("applicationWillTerminate:"); - // TODO: Notify every window that it will be destroyed, like done in iOS? + // TODO: Notify every window that it will be destroyed, like done in iOS? self.internal_exit(); } @@ -250,10 +250,10 @@ declare_class!( unsafe { let user_defaults: *mut Object = msg_send![class!(NSUserDefaults), standardUserDefaults]; - // The autofill heuristic controller causes slowdown and high CPU usage. - // We don't know exactly why. This disables the full heuristic controller. - // - // Adapted from: https://github.com/ghostty-org/ghostty/pull/8625 + // The autofill heuristic controller causes slowdown and high CPU usage. + // We don't know exactly why. This disables the full heuristic controller. + // + // Adapted from: https://github.com/ghostty-org/ghostty/pull/8625 let name = str_to_nsstring("NSAutoFillHeuristicControllerEnabled"); let existing_value: *mut Object = msg_send![user_defaults, objectForKey: name]; if existing_value.is_null() { @@ -282,7 +282,7 @@ declare_class!( } } - // Custom methods for menu actions + // Custom methods for menu actions unsafe impl ApplicationDelegate { #[method(rioCreateWindow:)] fn create_window(&self, _sender: Option<&AnyObject>) { @@ -403,13 +403,13 @@ impl ApplicationDelegate { // let workspace = &unsafe { NSWorkspace::sharedWorkspace() }; // let workspace_center = &unsafe { workspace.notificationCenter() }; // unsafe { - // workspace_center.addObserver_selector_name_object( - // &this, - // sel!(applicationDidUnhide:), - // // Some(ns_string!("NSWorkspaceDidActivateApplicationNotification")), - // Some(ns_string!("NSWorkspaceDidUnhideApplicationNotification")), - // Some(workspace), - // ) + // workspace_center.addObserver_selector_name_object( + // &this, + // sel!(applicationDidUnhide:), + // // Some(ns_string!("NSWorkspaceDidActivateApplicationNotification")), + // Some(ns_string!("NSWorkspaceDidUnhideApplicationNotification")), + // Some(workspace), + // ) // } unsafe { msg_send_id![super(this), init] } diff --git a/sugarloaf/src/context/metal.rs b/sugarloaf/src/context/metal.rs index f70e34a0..ce1dabb5 100644 --- a/sugarloaf/src/context/metal.rs +++ b/sugarloaf/src/context/metal.rs @@ -189,8 +189,8 @@ impl MetalContext { } // fn create_command_encoder(&self) -> Self::CommandEncoder { - // let command_buffer = self.command_queue.new_command_buffer().to_owned(); - // MetalCommandEncoder { command_buffer } + // let command_buffer = self.command_queue.new_command_buffer().to_owned(); + // MetalCommandEncoder { command_buffer } // } pub fn supports_f16(&self) -> bool { diff --git a/sugarloaf/src/font/macos.rs b/sugarloaf/src/font/macos.rs index 09d8220d..17dcf9e6 100644 --- a/sugarloaf/src/font/macos.rs +++ b/sugarloaf/src/font/macos.rs @@ -760,8 +760,8 @@ pub fn advance_units_for_char(handle: &FontHandle, ch: char) -> Option<(f32, u16 } /// Return the max advance width in pixels across all printable -/// ASCII (U+0020..U+007E) at `size_px`. Mirrors Ghostty's -/// cell-width derivation at `ghostty/src/font/face/coretext.zig:773-804`. +/// ASCII (U+0020..U+007E) at `size_px`. +/// cell-width derivation. /// /// Why ASCII-wide + max-of-all rather than just `space`: /// - Some fonts return `None` / glyph 0 for space and the caller @@ -1183,7 +1183,7 @@ fn build_utf16_to_utf8_map(text: &str) -> Vec { /// `CFStringCreateWithCharactersNoCopy`, and `ShapedGlyph.cluster` /// comes back as a UTF-16 code-unit offset. Skips the UTF-8 → UTF-16 /// conversion inside `CFString::new` AND the UTF-16 → UTF-8 mapping -/// pass after CoreText. Mirrors Ghostty's CoreText shaper at +/// pass after CoreText. CoreText shaper at /// `ghostty/src/font/shaper/coretext.zig:652-680`. pub fn shape_text_utf16( handle: &FontHandle, diff --git a/sugarloaf/src/font/nerd_font_attributes.rs b/sugarloaf/src/font/nerd_font_attributes.rs index 1c1c47c3..48b20c2d 100644 --- a/sugarloaf/src/font/nerd_font_attributes.rs +++ b/sugarloaf/src/font/nerd_font_attributes.rs @@ -11,7 +11,7 @@ //! arrows, Font Awesome icons, chevrons, etc.), the upstream Nerd Fonts //! patcher ships hand-tuned size / alignment / padding rules so that e.g. //! adjacent Powerline triangles butt together cleanly and icons stay -//! centered on their visual x-height. Ghostty compiled those rules out of +//! centered on their visual x-height. compiled those rules out of //! the patcher metadata; we reproduce them here verbatim so Rio's rendering //! of the same codepoints lines up with ghostty / Apple Terminal. //! diff --git a/sugarloaf/src/font/windows.rs b/sugarloaf/src/font/windows.rs index 2d33711b..2d596b22 100644 --- a/sugarloaf/src/font/windows.rs +++ b/sugarloaf/src/font/windows.rs @@ -12,7 +12,7 @@ //! Slower per cold lookup than DirectWrite (we touch every font file //! once until we hit a match), but cached at the font-cache layer so //! the cost amortizes to zero after the first hit per codepoint per -//! session. Ghostty doesn't ship Windows discovery yet either, so this +//! session. doesn't ship Windows discovery yet either, so this //! still puts us ahead. //! //! Future replacement path: when either `windows` or `dwrote` lands in diff --git a/sugarloaf/src/font_cache.rs b/sugarloaf/src/font_cache.rs index 459cf0a5..f52c52d6 100644 --- a/sugarloaf/src/font_cache.rs +++ b/sugarloaf/src/font_cache.rs @@ -161,7 +161,7 @@ pub(crate) fn compute_advance( } /// macOS variant: derive the advance from CoreText without ever touching -/// the font's raw bytes. Matches Ghostty's bytes-free font handling on +/// the font's raw bytes. bytes-free font handling on /// mac. #[cfg(target_os = "macos")] pub(crate) fn compute_advance( diff --git a/sugarloaf/src/grid/cell.rs b/sugarloaf/src/grid/cell.rs index a6854b0e..2835a63a 100644 --- a/sugarloaf/src/grid/cell.rs +++ b/sugarloaf/src/grid/cell.rs @@ -10,8 +10,8 @@ //! as raw bytes — `CellBg` and `CellText` are `#[repr(C)]` + bytemuck //! `Pod` so that's sound. //! -//! Layout is deliberately identical to Ghostty's `CellBg` / `CellText` -//! (`ghostty/src/renderer/metal/shaders.zig:265-291`) so the shader +//! Layout is deliberately identical to `CellBg` / `CellText` +//! so the shader //! port in Phase 1 can stay a near-1:1 translation. use bytemuck::{Pod, Zeroable}; @@ -22,7 +22,7 @@ use bytemuck::{Pod, Zeroable}; /// /// Selection / search / inverse-video tinting is folded into this value /// on the CPU side before upload — there are no shader-side bits for -/// selection state. Same approach as Ghostty (`generic.zig:2823`). +/// selection state. same approach. #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)] pub struct CellBg { @@ -39,7 +39,7 @@ impl CellBg { /// GPU. A single terminal cell may emit multiple `CellText`s: one for /// the base glyph, plus one per decoration (underline / strikethrough /// / overline / curly underline / hyperlink underline). Matches -/// Ghostty's approach where decorations are separate `CellText` +/// approach where decorations are separate `CellText` /// entries rather than bit-packed onto the base glyph. /// /// Total size is **32 bytes** — verified by the `size_of` const assert @@ -53,7 +53,7 @@ pub struct CellText { /// Glyph bitmap size (w, h) in atlas pixels. pub glyph_size: [u32; 2], /// Font bearings (x-left, y-bottom), relative to the grid cell - /// origin, in pixels. `i16` matches Ghostty; if Rio's font metrics + /// origin, in pixels. `i16` matches ; if Rio's font metrics /// ever exceed ±32k pixels we have bigger problems. pub bearings: [i16; 2], /// Terminal grid position (col, row). `u16` caps at 65535 which @@ -63,8 +63,8 @@ pub struct CellText { /// dim + selection/search tint have been applied CPU-side). pub color: [u8; 4], /// Atlas discriminator: - /// 0 = grayscale (sampled as alpha mask, multiplied by `color`) - /// 1 = color (sampled directly, `color` ignored) + /// 0 = grayscale (sampled as alpha mask, multiplied by `color`) + /// 1 = color (sampled directly, `color` ignored) pub atlas: u8, /// Packed bits. bit 0 = `no_min_contrast`, bit 1 = `is_cursor_glyph`. /// Unused bits reserved for future decoration flags. @@ -92,8 +92,8 @@ const _: () = { /// Per-frame uniform block bound to both bg and text pipelines. /// -/// Field order / sizes mirror Ghostty's `Uniforms` struct -/// (`ghostty/src/renderer/metal/shaders.zig` — search `Uniforms`). The +/// Field order / sizes mirror `Uniforms` struct +///. The /// layout is hand-packed so the same bytes round-trip through both /// Metal (`setVertexBytes`) and wgpu (`write_buffer` into a `uniform` /// binding). If you add a field: @@ -141,23 +141,23 @@ pub struct GridUniforms { /// Minimum WCAG contrast ratio to enforce against bg. `0.0` disables. pub min_contrast: f32, /// Bool flags packed into u32 for WGSL compatibility: - /// bit 0 = display_p3 colorspace tag - /// bit 1 = linear_blending + /// bit 0 = display_p3 colorspace tag + /// bit 1 = linear_blending pub flags: u32, /// Padding-extend bitfield (bit 0 = left, 1 = right, 2 = up, 3 = down). pub padding_extend: u32, /// How the shader should interpret the sRGB-encoded CPU color /// inputs before writing to the DisplayP3-tagged drawable. - /// - `0` = sRGB (apply sRGB → DisplayP3 primaries matrix) + /// - `0` = sRGB (apply sRGB → DisplayP3 primaries matrix) /// - `1` = DisplayP3 (already P3, skip matrix) - /// - `2` = Rec.2020 (apply Rec.2020 → DisplayP3 matrix) + /// - `2` = Rec.2020 (apply Rec.2020 → DisplayP3 matrix) /// /// Wired to the same source as sugarloaf's rich-text quad /// `input_colorspace` (`renderer/mod.rs:264`), so the grid and /// every other pipeline agree on the transform. Without this the /// grid bg appears brighter/more saturated than the window bg /// fill, which runs through `prepare_output_rgb`. Mirrors - /// Ghostty's `Uniforms.use_display_p3` + `load_color` pair at + /// `Uniforms.use_display_p3` + `load_color` pair at /// `ghostty/src/renderer/shaders/shaders.metal:224`. pub input_colorspace: u32, } diff --git a/sugarloaf/src/grid/cpu.rs b/sugarloaf/src/grid/cpu.rs index c063b387..6bb7991c 100644 --- a/sugarloaf/src/grid/cpu.rs +++ b/sugarloaf/src/grid/cpu.rs @@ -245,6 +245,33 @@ impl CpuGridRenderer { } } + pub fn set_block_cursor(&mut self, cells: &[CellText]) { + if let Some(slot) = self.fg_rows.first_mut() { + slot.clear(); + slot.extend_from_slice(cells); + } + } + + pub fn set_non_block_cursor(&mut self, cells: &[CellText]) { + let idx = self.fg_rows.len().saturating_sub(1); + if let Some(slot) = self.fg_rows.get_mut(idx) { + slot.clear(); + slot.extend_from_slice(cells); + } + } + + pub fn clear_cursor(&mut self) { + if let Some(slot) = self.fg_rows.first_mut() { + slot.clear(); + } + let last = self.fg_rows.len().saturating_sub(1); + if last > 0 { + if let Some(slot) = self.fg_rows.get_mut(last) { + slot.clear(); + } + } + } + #[inline] pub fn lookup_glyph(&self, key: GlyphKey) -> Option { self.atlas_grayscale.lookup(key) diff --git a/sugarloaf/src/grid/metal.rs b/sugarloaf/src/grid/metal.rs index 0d442adb..7e8d65c0 100644 --- a/sugarloaf/src/grid/metal.rs +++ b/sugarloaf/src/grid/metal.rs @@ -11,7 +11,7 @@ //! add a GPU completion handler + semaphore gate. For now slot 0 is //! written and read on every frame. //! -//! Mirrors Ghostty's `ghostty/src/renderer/cell.zig` allocation model: +//! `ghostty/src/renderer/cell.zig` allocation model: //! one flat `CellBg` buffer indexed `row * cols + col`, one //! `ArrayList(CellText)` per row plus two cursor slots. The per-row //! FG storage lands in Phase 1c alongside `cell_text` shader port. @@ -35,13 +35,13 @@ use crate::renderer::image_cache::atlas::AtlasAllocator; const FRAMES_IN_FLIGHT: usize = 3; /// Extra slots appended to the per-row fg storage for cursor glyphs. -/// Matches Ghostty's `rows + 2` layout (block cursor at slot 0, +/// `rows + 2` layout (block cursor at slot 0, /// non-block-style cursor at the tail). const CURSOR_ROW_SLOTS: usize = 2; /// Initial square atlas texture side. 2048² @ R8 = 4 MiB, grown to /// 4096² / 8192² on demand when the allocator reports full (see -/// `MetalGlyphAtlas::grow`). Mirrors Ghostty's `atlas.grow` in +/// `MetalGlyphAtlas::grow`). `atlas.grow` in /// `ghostty/src/font/Atlas.zig`. const ATLAS_SIZE: u16 = 2048; @@ -53,9 +53,9 @@ const ATLAS_MAX_SIZE: u16 = 8192; /// Glyph atlas for grayscale OR color glyphs. A single instance /// holds one `MTLTexture`, an allocator, and the key→slot map; the /// `bytes_per_pixel` field lets the same struct serve both paths -/// (R8 for mask glyphs, RGBA8 for color emoji). Mirrors Ghostty's +/// (R8 for mask glyphs, RGBA8 for color emoji). /// split between `atlas_grayscale` and `atlas_color` -/// (`ghostty/src/renderer/cell.zig`) — owned by the renderer rather +/// — owned by the renderer rather /// than the font subsystem. pub struct MetalGlyphAtlas { pub(crate) texture: Texture, @@ -227,7 +227,7 @@ fn create_atlas_texture( // the texture in shared memory — `replaceRegion` becomes a plain // memcpy with no CPU/GPU coherency sync. Discrete-GPU Macs // (pre-M1) still need `Managed` with an implicit sync on draw. - // Matches Ghostty `src/renderer/Metal.zig:79-83`. + // `src/renderer/Metal.zig:79-83`. descriptor.set_storage_mode(if device.has_unified_memory() { metal::MTLStorageMode::Shared } else { @@ -273,16 +273,16 @@ pub struct MetalGridRenderer { frame: usize, /// Compiled bg render pipeline. Binds: - /// buffer(0): `GridUniforms` (via `set_vertex_bytes` / - /// `set_fragment_bytes`) - /// buffer(1): `bg_buffers[0]` + /// buffer(0): `GridUniforms` (via `set_vertex_bytes` / + /// `set_fragment_bytes`) + /// buffer(1): `bg_buffers[0]` bg_pipeline: RenderPipelineState, /// Compiled text render pipeline. Binds: - /// buffer(0): per-instance `CellText` vertex buffer - /// buffer(1): `GridUniforms` - /// texture(0): `atlas_grayscale` - /// texture(1): `atlas_color` (reused = atlas_grayscale for now) + /// buffer(0): per-instance `CellText` vertex buffer + /// buffer(1): `GridUniforms` + /// texture(0): `atlas_grayscale` + /// texture(1): `atlas_color` (reused = atlas_grayscale for now) text_pipeline: RenderPipelineState, /// Staging buffer for the concatenated fg instances. Rebuilt each @@ -316,7 +316,7 @@ pub struct MetalGridRenderer { /// Set to `true` on construction + `resize()`. The emission /// path checks this to force a full rebuild (every row) on the /// next frame, regardless of whether `TerminalDamage` is - /// `Noop`. Mirrors Ghostty's `grid_size_diff` gate at + /// `Noop`. `grid_size_diff` gate at /// `ghostty/src/renderer/generic.zig:2353`. Cleared via /// `mark_full_rebuild_done` after the emission loop runs. needs_full_rebuild: bool, @@ -484,12 +484,70 @@ impl MetalGridRenderer { } } + /// Replace the block cursor sprite slot. Drawn FIRST in the text + /// pass (slot 0) — sits BEHIND row glyphs so text inversion can + /// composite on top of the block. "block" cursor + /// slot at `fg_rows[0]`. + pub fn set_block_cursor(&mut self, cells: &[CellText]) { + if let Some(slot) = self.fg_rows.first_mut() { + if slot.is_empty() && cells.is_empty() { + return; + } + slot.clear(); + slot.extend_from_slice(cells); + self.fg_dirty = true; + } + } + + /// Replace the non-block cursor sprite slot. Drawn LAST in the + /// text pass — sits on top of all row glyphs. Used for hollow / + /// bar / underline cursor sprites that should overlay text. Pass + /// `&[]` to clear. "non-block" cursor slot at + /// `fg_rows[rows + 1]`. + pub fn set_non_block_cursor(&mut self, cells: &[CellText]) { + let idx = self.fg_rows.len().saturating_sub(1); + if let Some(slot) = self.fg_rows.get_mut(idx) { + if slot.is_empty() && cells.is_empty() { + return; + } + slot.clear(); + slot.extend_from_slice(cells); + self.fg_dirty = true; + } + } + + /// Empty both cursor slots (block + non-block). Call once per + /// frame before deciding whether to emit a cursor sprite for + /// this panel — without this, the previous frame's sprite stays + /// resident in fg_rows. + pub fn clear_cursor(&mut self) { + let mut changed = false; + if let Some(slot) = self.fg_rows.first_mut() { + if !slot.is_empty() { + slot.clear(); + changed = true; + } + } + let last = self.fg_rows.len().saturating_sub(1); + if last > 0 { + if let Some(slot) = self.fg_rows.get_mut(last) { + if !slot.is_empty() { + slot.clear(); + changed = true; + } + } + } + if changed { + self.fg_dirty = true; + } + } + /// Record both grid passes against the caller's `encoder`. The /// caller owns the command buffer, drawable, and render pass /// descriptor. Draw order: /// - /// 1. bg pass — fullscreen triangle, per-fragment cell lookup. - /// 2. text pass — one instanced quad per `CellText` in `fg_rows`. + /// 1. bg pass — fullscreen triangle, per-fragment cell lookup. + /// 2. text pass — one instanced quad per `CellText` in `fg_rows`. pub fn render(&mut self, encoder: &RenderCommandEncoderRef, uniforms: &GridUniforms) { let uniforms_bytes = bytemuck::bytes_of(uniforms); @@ -518,7 +576,7 @@ impl MetalGridRenderer { if self.fg_dirty { // Flatten per-row fg_rows into the staging vec. Order matters // for z: slot 0 (block cursor) first, content rows next, - // non-block-cursor slot last — same as Ghostty's ordering. + // non-block-cursor slot last — same approach's ordering. self.fg_staging.clear(); for row in &self.fg_rows { self.fg_staging.extend_from_slice(row); @@ -641,7 +699,7 @@ fn build_text_pipeline(device: &Device) -> RenderPipelineState { .expect("color attachment 0 missing"); color.set_pixel_format(MTLPixelFormat::BGRA8Unorm); color.set_blending_enabled(true); - // Premultiplied-over, matching Ghostty (Pipeline.zig:130-133). + // Premultiplied-over, matching . // The text fragment returns `in.color * mask_a` (grayscale path) // or the color-atlas sample directly (emoji) — both premultiplied // already, so source RGB factor must be `One`, not `SourceAlpha`. diff --git a/sugarloaf/src/grid/mod.rs b/sugarloaf/src/grid/mod.rs index 762d92fe..5be84666 100644 --- a/sugarloaf/src/grid/mod.rs +++ b/sugarloaf/src/grid/mod.rs @@ -8,7 +8,7 @@ //! This is the target of a direct rewrite replacing sugarloaf's //! rich-text-based terminal rendering (see //! `memory/project_grid_gpu_renderer_plan.md`). The design mirrors -//! Ghostty's Metal renderer: one `bg_cells` flat buffer indexed by +//! Metal renderer: one `bg_cells` flat buffer indexed by //! `row * cols + col`, one `fg_rows` collection of per-row glyph lists //! concatenated for GPU upload. Only dirty rows are rewritten between //! frames; everything else stays resident in the GPU buffer. @@ -118,8 +118,8 @@ impl GridRenderer { } } - /// Zero out `row`'s fg/bg slots. Corresponds to Ghostty's - /// `self.cells.clear(y)` (`generic.zig:2436`). + /// Zero out `row`'s fg/bg slots. Corresponds to 's + /// `self.cells.clear(y)`. pub fn clear_row(&mut self, row: u32) { match self { #[cfg(target_os = "macos")] @@ -132,6 +132,54 @@ impl GridRenderer { } } + /// Replace the block cursor sprite slot. Drawn FIRST in the text + /// pass (`fg_rows[0]`) — sits BEHIND row glyphs so text inversion + /// composites on top of the block. + /// `Contents.setCursor` for the `.block` style. + pub fn set_block_cursor(&mut self, cells: &[CellText]) { + match self { + #[cfg(target_os = "macos")] + GridRenderer::Metal(r) => r.set_block_cursor(cells), + #[cfg(feature = "wgpu")] + GridRenderer::Wgpu(r) => r.set_block_cursor(cells), + #[cfg(target_os = "linux")] + GridRenderer::Vulkan(r) => r.set_block_cursor(cells), + GridRenderer::Cpu(r) => r.set_block_cursor(cells), + } + } + + /// Replace the non-block cursor sprite slot. Drawn on top of all + /// row glyphs in the text pass — used for hollow / bar / + /// underline cursor sprites that overlay text. Pass `&[]` to + /// clear. `Contents.setCursor` writing into + /// `fg_rows[rows + 1]`. + pub fn set_non_block_cursor(&mut self, cells: &[CellText]) { + match self { + #[cfg(target_os = "macos")] + GridRenderer::Metal(r) => r.set_non_block_cursor(cells), + #[cfg(feature = "wgpu")] + GridRenderer::Wgpu(r) => r.set_non_block_cursor(cells), + #[cfg(target_os = "linux")] + GridRenderer::Vulkan(r) => r.set_non_block_cursor(cells), + GridRenderer::Cpu(r) => r.set_non_block_cursor(cells), + } + } + + /// Empty both cursor slots (block + non-block). Call once per + /// frame before deciding whether to emit a cursor sprite for + /// this panel. + pub fn clear_cursor(&mut self) { + match self { + #[cfg(target_os = "macos")] + GridRenderer::Metal(r) => r.clear_cursor(), + #[cfg(feature = "wgpu")] + GridRenderer::Wgpu(r) => r.clear_cursor(), + #[cfg(target_os = "linux")] + GridRenderer::Vulkan(r) => r.clear_cursor(), + GridRenderer::Cpu(r) => r.clear_cursor(), + } + } + /// Record grid draw calls against a caller-supplied render pass / /// encoder. The caller owns the command buffer + drawable + pass /// descriptor so the grid composes with sugarloaf's UI overlays diff --git a/sugarloaf/src/grid/shaders/grid.metal b/sugarloaf/src/grid/shaders/grid.metal index 7f43604d..70d2c1fa 100644 --- a/sugarloaf/src/grid/shaders/grid.metal +++ b/sugarloaf/src/grid/shaders/grid.metal @@ -6,15 +6,15 @@ // Metal shader for the grid renderer. // // Ported from `ghostty/src/renderer/shaders/shaders.metal`: -// - full_screen_vertex (line 191 in upstream) -// - cell_bg_fragment (line 451) +// - full_screen_vertex (line 191 in upstream) +// - cell_bg_fragment (line 451) // // Phase 1a scope: bg pass only. `cell_text_*` (text pass) is ported in // Phase 1c. Color space conversion is deliberately minimal right now: // the CAMetalLayer in `sugarloaf/src/context/metal.rs` is tagged // DisplayP3 and `setPresentsWithTransaction:false`, and we emit cell // colors pre-multiplied in sRGB-gamma space to match the -// non-linear-blending default. The full Ghostty `load_color` chain +// non-linear-blending default. The full `load_color` chain // (linearize → sRGB_DP3 → unlinearize) lands when we add the // `use_display_p3` / `use_linear_blending` uniform paths. @@ -43,7 +43,7 @@ struct Uniforms { // Color space / transfer curve helpers. Matrices match // `sugarloaf/src/renderer/renderer.metal` (Bradford-adapted D65) so // the grid's output is byte-identical to sugarloaf's quad pipeline -// (`draw_bg_fill_metal`, UI overlays). Same role as Ghostty's +// (`draw_bg_fill_metal`, UI overlays). Same role as 's // `linearize` / `unlinearize` / `srgb_to_display_p3` at // `ghostty/src/renderer/shaders/shaders.metal:57-85`. //------------------------------------------------------------------- @@ -79,7 +79,7 @@ float3 grid_rec2020_to_p3(float3 linear_r2020) { /// `input_colorspace`) → sRGB-encode again. Every shader output goes /// through this so the framebuffer (BGRA8Unorm tagged DisplayP3) /// stores gamma-encoded values the compositor can display directly -/// and alpha blending runs in gamma space — matching Ghostty's +/// and alpha blending runs in gamma space — matching 's /// `alpha-blending = native` default. float3 grid_prepare_output_rgb(float3 srgb, uint input_colorspace) { float3 lin = grid_srgb_to_linear(srgb); @@ -102,7 +102,7 @@ constant uint PAD_EXTEND_DOWN = 1u << 3; // Per-cell background. One uchar4 per grid cell, indexed // `row * grid_size.x + col`. Matches `CellBg` in cell.rs (4 bytes). // Declared `constant` so Metal places it in constant address space — -// same as Ghostty's `constant uchar4 *cells` parameter at +// same approach's `constant uchar4 *cells` parameter at // `shaders.metal:454`. //------------------------------------------------------------------- @@ -116,10 +116,10 @@ struct FullScreenVertexOut { vertex FullScreenVertexOut grid_bg_vertex(uint vid [[vertex_id]]) { FullScreenVertexOut out; - // Single triangle clipped to viewport. - // vid 0: (-1, -3) - // vid 1: (-1, 1) - // vid 2: ( 3, 1) + // Single triangle clipped to viewport. + // vid 0: (-1, -3) + // vid 1: (-1, 1) + // vid 2: ( 3, 1) float4 position; position.x = (vid == 2) ? 3.0 : -1.0; position.y = (vid == 0) ? -3.0 : 1.0; @@ -138,9 +138,9 @@ fragment float4 grid_bg_fragment( constant Uniforms& uniforms [[buffer(0)]], constant uchar4* cells [[buffer(1)]] ) { - // `in.position.xy` is the pixel's center in framebuffer pixels. - // `grid_padding` is (top, right, bottom, left) — we only need - // left (.w) and top (.x) to locate the grid origin. + // `in.position.xy` is the pixel's center in framebuffer pixels. + // `grid_padding` is (top, right, bottom, left) — we only need + // left (.w) and top (.x) to locate the grid origin. int2 orig_grid_pos = int2( floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size) ); @@ -148,7 +148,7 @@ fragment float4 grid_bg_fragment( float4 bg = float4(0.0); - // Horizontal padding: clamp or discard based on padding_extend bits. + // Horizontal padding: clamp or discard based on padding_extend bits. if (grid_pos.x < 0) { if (uniforms.padding_extend & PAD_EXTEND_LEFT) { grid_pos.x = 0; @@ -163,7 +163,7 @@ fragment float4 grid_bg_fragment( } } - // Vertical padding. + // Vertical padding. if (grid_pos.y < 0) { if (uniforms.padding_extend & PAD_EXTEND_UP) { grid_pos.y = 0; @@ -178,11 +178,11 @@ fragment float4 grid_bg_fragment( } } - // Cursor overlay: paint `cursor_bg_color` only when this fragment - // is inside the actual cursor cell (compare against the original, - // pre-padding-clamp grid_pos). This keeps the cursor from - // leaking into the window margin when it sits on an edge row / - // column and `padding_extend` clamps inward to the cursor cell. + // Cursor overlay: paint `cursor_bg_color` only when this fragment + // is inside the actual cursor cell (compare against the original, + // pre-padding-clamp grid_pos). This keeps the cursor from + // leaking into the window margin when it sits on an edge row / + // column and `padding_extend` clamps inward to the cursor cell. if (uniforms.cursor_bg_color.a > 0.0 && orig_grid_pos.x == int(uniforms.cursor_pos.x) && orig_grid_pos.y == int(uniforms.cursor_pos.y)) @@ -193,7 +193,7 @@ fragment float4 grid_bg_fragment( return c; } - // Load the cell and convert to normalized premultiplied color. + // Load the cell and convert to normalized premultiplied color. uchar4 cell = cells[grid_pos.y * int(uniforms.grid_size.x) + grid_pos.x]; float4 color = float4(cell) / 255.0; color.rgb = grid_prepare_output_rgb(color.rgb, uniforms.input_colorspace); @@ -207,10 +207,10 @@ fragment float4 grid_bg_fragment( // // Ported from `ghostty/src/renderer/shaders/shaders.metal:525-761`. // Phase 1c simplifications: -// - No Display P3 / linear-blending conversions; colors land -// already sRGB-encoded. -// - No WCAG min_contrast enforcement. -// - No `cursor_wide` handling (single-cell cursor only for now). +// - No Display P3 / linear-blending conversions; colors land +// already sRGB-encoded. +// - No WCAG min_contrast enforcement. +// - No `cursor_wide` handling (single-cell cursor only for now). //------------------------------------------------------------------- constant uint ATLAS_GRAYSCALE = 0u; @@ -240,62 +240,68 @@ struct CellTextVertexOut { // // Triangle-strip-like quad via a 3-vertex triangle per instance. We use a -// 4-vertex triangle-strip input (vid = 0..3) — same pattern as Ghostty's +// 4-vertex triangle-strip input (vid = 0..3) — same pattern as 's // shader to avoid redundant vertex shader invocations. // -// 0 --> 1 -// | .'| -// | / | -// | L | -// 2 --> 3 +// 0 --> 1 +// | .'| +// | / | +// | L | +// 2 --> 3 // vertex CellTextVertexOut grid_text_vertex( uint vid [[vertex_id]], CellTextVertexIn in [[stage_in]], constant Uniforms& uniforms [[buffer(1)]] ) { - // Cell origin in pixel space. + // Cell origin in pixel space. float2 cell_pos = uniforms.cell_size * float2(in.grid_pos); - // Quad corner (0..1 in each dim) from vertex id. + // Quad corner (0..1 in each dim) from vertex id. float2 corner; corner.x = float(vid == 1 || vid == 3); corner.y = float(vid == 2 || vid == 3); - // Glyph bbox inside the cell: bearings.x from left, bearings.y from - // bottom (font convention). See Ghostty diagram at shaders.metal:587. + // Glyph bbox inside the cell: bearings.x from left, bearings.y from + // bottom (font convention). See diagram at shaders.metal:587. float2 size = float2(in.glyph_size); float2 offset = float2(in.bearings); offset.y = uniforms.cell_size.y - offset.y; float2 quad = cell_pos + size * corner + offset; - // Also shift by grid_padding (top/left) to position the whole grid - // inside the drawable — `grid_padding` is (top, right, bottom, left). + // Also shift by grid_padding (top/left) to position the whole grid + // inside the drawable — `grid_padding` is (top, right, bottom, left). quad.x += uniforms.grid_padding.w; quad.y += uniforms.grid_padding.x; CellTextVertexOut out; out.position = uniforms.projection * float4(quad.x, quad.y, 0.0, 1.0); - // Atlas tex coords in pixel space (the sampler is set to - // `coord::pixel`, so no normalization needed). + // Atlas tex coords in pixel space (the sampler is set to + // `coord::pixel`, so no normalization needed). out.tex_coord = float2(in.glyph_pos) + float2(in.glyph_size) * corner; out.atlas = uint(in.atlas); - // Foreground color — u8 → float, convert to output space, then - // premultiply. Same pipeline as `grid_bg_fragment` so glyph and - // cell bg agree. + // Foreground color — u8 → float, convert to output space, then + // premultiply. Same pipeline as `grid_bg_fragment` so glyph and + // cell bg agree. float4 color = float4(in.color) / 255.0; color.rgb = grid_prepare_output_rgb(color.rgb, uniforms.input_colorspace); color.rgb *= color.a; - // Cursor-pos fg swap: if this glyph's cell is under the cursor and - // it is *not* itself the cursor glyph, use `cursor_color` instead. + // Cursor-pos fg swap: if this glyph's cell is under the cursor and + // it is *not* itself the cursor glyph, use `cursor_color` instead. bool is_cursor_pos = (uint(in.grid_pos.x) == uniforms.cursor_pos.x) && (uint(in.grid_pos.y) == uniforms.cursor_pos.y); - if ((in.bools & BOOL_IS_CURSOR_GLYPH) == 0u && is_cursor_pos) { + // The fg-swap only fires when an explicit cursor_color was + // supplied. Hollow / unfocused cursors skip this by setting + // `cursor_color.a = 0`, leaving the underlying glyph colour + // intact (`.block_hollow` path). + if ((in.bools & BOOL_IS_CURSOR_GLYPH) == 0u + && is_cursor_pos + && uniforms.cursor_color.a > 0.0) { color = uniforms.cursor_color; color.rgb = grid_prepare_output_rgb(color.rgb, uniforms.input_colorspace); color.rgb *= color.a; @@ -317,11 +323,11 @@ fragment float4 grid_text_fragment( ); if (in.atlas == ATLAS_GRAYSCALE) { - // Grayscale atlas: r channel is the alpha mask, multiply by color. + // Grayscale atlas: r channel is the alpha mask, multiply by color. float a = atlas_grayscale.sample(atlas_sampler, in.tex_coord).r; return in.color * a; } else { - // Color atlas: pre-multiplied RGBA directly. + // Color atlas: pre-multiplied RGBA directly. return atlas_color.sample(atlas_sampler, in.tex_coord); } } @@ -353,8 +359,8 @@ vertex CellTextVertexOut text_vertex( TextVertexIn in [[stage_in]], constant float2& viewport [[buffer(1)]] ) { - // Quad corner 0..1 in each dim, from vertex id. Matches the - // triangle-strip-4 pattern in `grid_text_vertex`. + // Quad corner 0..1 in each dim, from vertex id. Matches the + // triangle-strip-4 pattern in `grid_text_vertex`. float2 corner; corner.x = float(vid == 1 || vid == 3); corner.y = float(vid == 2 || vid == 3); @@ -363,7 +369,7 @@ vertex CellTextVertexOut text_vertex( float2 origin = in.pos + float2(in.bearings); float2 quad_px = origin + size * corner; - // Pixel → NDC (y-flip so `pos.y` grows downward in screen space). + // Pixel → NDC (y-flip so `pos.y` grows downward in screen space). float2 ndc = float2( (quad_px.x / viewport.x) * 2.0 - 1.0, 1.0 - (quad_px.y / viewport.y) * 2.0 @@ -374,7 +380,7 @@ vertex CellTextVertexOut text_vertex( out.tex_coord = float2(in.glyph_pos) + size * corner; out.atlas = uint(in.atlas); - // Premultiplied RGBA. Matches the grid text path's blend model. + // Premultiplied RGBA. Matches the grid text path's blend model. float4 color = float4(in.color) / 255.0; color.rgb *= color.a; out.color = color; diff --git a/sugarloaf/src/grid/shaders/grid.wgsl b/sugarloaf/src/grid/shaders/grid.wgsl index 760a3beb..5bfcf0f3 100644 --- a/sugarloaf/src/grid/shaders/grid.wgsl +++ b/sugarloaf/src/grid/shaders/grid.wgsl @@ -6,16 +6,16 @@ // WGSL grid shader. Peer of `grid.metal`. // // Ported from `ghostty/src/renderer/shaders/shaders.metal`: -// - full_screen_vertex (line 191 in upstream) -// - cell_bg_fragment (line 451) +// - full_screen_vertex (line 191 in upstream) +// - cell_bg_fragment (line 451) // // Phase 1b scope: bg pass only. Same simplifications as the Metal // port — no full Display P3 / linear-blending chain yet (the colors // come in already sRGB-encoded from the CPU). // // Bindings: -// @group(0) @binding(0) Uniforms (140+4 = 144 bytes) -// @group(0) @binding(1) CellBg[] (cols * rows entries) +// @group(0) @binding(0) Uniforms (140+4 = 144 bytes) +// @group(0) @binding(1) CellBg[] (cols * rows entries) // // Must match `WgpuGridRenderer`'s bind group layout in // `sugarloaf/src/grid/webgpu.rs`. @@ -97,10 +97,10 @@ struct VsOut { @vertex fn grid_bg_vertex(@builtin(vertex_index) vid: u32) -> VsOut { - // Fullscreen triangle (same trick as the Metal port). - // vid 0: (-1, -3) - // vid 1: (-1, 1) - // vid 2: ( 3, 1) + // Fullscreen triangle (same trick as the Metal port). + // vid 0: (-1, -3) + // vid 1: (-1, 1) + // vid 2: ( 3, 1) var x = -1.0; var y = 1.0; if (vid == 2u) { x = 3.0; } @@ -112,26 +112,26 @@ fn grid_bg_vertex(@builtin(vertex_index) vid: u32) -> VsOut { } fn load_cell_bg(idx: u32) -> vec4 { - // One u32 per cell; unpack RGBA little-endian bytes. + // One u32 per cell; unpack RGBA little-endian bytes. let word = cells[idx]; let r = f32((word >> 0u) & 0xFFu) / 255.0; let g = f32((word >> 8u) & 0xFFu) / 255.0; let b = f32((word >> 16u) & 0xFFu) / 255.0; let a = f32((word >> 24u) & 0xFFu) / 255.0; - // Premultiply. + // Premultiply. return vec4(r * a, g * a, b * a, a); } @fragment fn grid_bg_fragment(in: VsOut) -> @location(0) vec4 { - // `grid_padding` is (top, right, bottom, left). - // Use .w (left) + .x (top) to find the grid origin, same as Metal port. + // `grid_padding` is (top, right, bottom, left). + // Use .w (left) + .x (top) to find the grid origin, same as Metal port. let cell_fx = (in.position.xy - vec2(uniforms.grid_padding.w, uniforms.grid_padding.x)) / uniforms.cell_size; let orig_grid_pos = vec2(floor(cell_fx)); var grid_pos = orig_grid_pos; - // Horizontal padding. + // Horizontal padding. let cols = i32(uniforms.grid_size.x); if (grid_pos.x < 0) { if ((uniforms.padding_extend & PAD_EXTEND_LEFT) != 0u) { @@ -147,7 +147,7 @@ fn grid_bg_fragment(in: VsOut) -> @location(0) vec4 { } } - // Vertical padding. + // Vertical padding. let rows = i32(uniforms.grid_size.y); if (grid_pos.y < 0) { if ((uniforms.padding_extend & PAD_EXTEND_UP) != 0u) { @@ -163,9 +163,9 @@ fn grid_bg_fragment(in: VsOut) -> @location(0) vec4 { } } - // Cursor overlay at in-bounds cursor cell only (skip - // padding-extended fragments so an edge cursor doesn't bleed - // into the window margin). + // Cursor overlay at in-bounds cursor cell only (skip + // padding-extended fragments so an edge cursor doesn't bleed + // into the window margin). if (uniforms.cursor_bg_color.a > 0.0 && orig_grid_pos.x == i32(uniforms.cursor_pos.x) && orig_grid_pos.y == i32(uniforms.cursor_pos.y)) { @@ -177,10 +177,10 @@ fn grid_bg_fragment(in: VsOut) -> @location(0) vec4 { return vec4(rgb * a, a); } - // Load cell, convert to output color space, then premultiply. - // Same pipeline as the quad fill in `sugarloaf/src/renderer/renderer.metal` - // so the grid and window-fill paths produce identical framebuffer - // values. + // Load cell, convert to output color space, then premultiply. + // Same pipeline as the quad fill in `sugarloaf/src/renderer/renderer.metal` + // so the grid and window-fill paths produce identical framebuffer + // values. let idx = u32(grid_pos.y) * uniforms.grid_size.x + u32(grid_pos.x); let word = cells[idx]; let r = f32((word >> 0u) & 0xFFu) / 255.0; @@ -206,8 +206,8 @@ const BOOL_NO_MIN_CONTRAST: u32 = 1u; const BOOL_IS_CURSOR_GLYPH: u32 = 2u; struct CellTextVertexIn { - // Per-instance attributes (attribute locations match the wgpu - // vertex buffer layout in grid/webgpu.rs). + // Per-instance attributes (attribute locations match the wgpu + // vertex buffer layout in grid/webgpu.rs). @location(0) glyph_pos: vec2, @location(1) glyph_size: vec2, @location(2) bearings: vec2, @@ -236,53 +236,56 @@ fn grid_text_vertex( @builtin(vertex_index) vid: u32, in: CellTextVertexIn, ) -> TextVsOut { - // Cell origin in pixel space. + // Cell origin in pixel space. let cell_pos = uniforms.cell_size * vec2(in.grid_pos); - // Quad corner (0..1) from vertex id — 4-vertex triangle strip. - // 0 --> 1 - // | .'| - // | / | - // | L | - // 2 --> 3 + // Quad corner (0..1) from vertex id — 4-vertex triangle strip. + // 0 --> 1 + // | .'| + // | / | + // | L | + // 2 --> 3 var corner: vec2; corner.x = select(0.0, 1.0, vid == 1u || vid == 3u); corner.y = select(0.0, 1.0, vid == 2u || vid == 3u); - // Glyph bbox inside cell: bearings.x from left, bearings.y from - // bottom (font convention). + // Glyph bbox inside cell: bearings.x from left, bearings.y from + // bottom (font convention). let size = vec2(in.glyph_size); var offset = vec2(in.bearings); offset.y = uniforms.cell_size.y - offset.y; var quad = cell_pos + size * corner + offset; - // Shift by grid_padding (top/left). + // Shift by grid_padding (top/left). quad.x += uniforms.grid_padding.w; quad.y += uniforms.grid_padding.x; var out: TextVsOut; out.position = uniforms.projection * vec4(quad, 0.0, 1.0); - // Atlas tex coords in PIXEL space — sampler is set to nearest, - // unnormalized coords equivalent via textureLoad below. + // Atlas tex coords in PIXEL space — sampler is set to nearest, + // unnormalized coords equivalent via textureLoad below. out.tex_coord = vec2(in.glyph_pos) + vec2(in.glyph_size) * corner; out.atlas = in.atlas; - // Foreground color — `in.color` arrives normalized via UNorm8x4. - // Convert to output color space first, then premultiply. Same - // pipeline as `grid_bg_fragment` and the quad fill so glyph/cell - // bg/window bg agree. + // Foreground color — `in.color` arrives normalized via UNorm8x4. + // Convert to output color space first, then premultiply. Same + // pipeline as `grid_bg_fragment` and the quad fill so glyph/cell + // bg/window bg agree. var color = in.color; color = vec4( grid_prepare_output_rgb(color.rgb, uniforms.input_colorspace) * color.a, color.a, ); - // Cursor-pos fg swap. + // Cursor-pos fg swap. Skip when cursor_color.a == 0 — that's the + // hollow / unfocused path where text colour stays untouched. let is_cursor_pos = in.grid_pos.x == uniforms.cursor_pos.x && in.grid_pos.y == uniforms.cursor_pos.y; - if ((in.bools & BOOL_IS_CURSOR_GLYPH) == 0u && is_cursor_pos) { + if ((in.bools & BOOL_IS_CURSOR_GLYPH) == 0u + && is_cursor_pos + && uniforms.cursor_color.a > 0.0) { let c = uniforms.cursor_color; color = vec4( grid_prepare_output_rgb(c.rgb, uniforms.input_colorspace) * c.a, @@ -296,8 +299,8 @@ fn grid_text_vertex( @fragment fn grid_text_fragment(in: TextVsOut) -> @location(0) vec4 { - // Pixel-space tex_coord → integer sample via textureLoad (no - // sampler filter; matches Metal's `coord::pixel` + `filter::nearest`). + // Pixel-space tex_coord → integer sample via textureLoad (no + // sampler filter; matches Metal's `coord::pixel` + `filter::nearest`). let ic = vec2(in.tex_coord); if (in.atlas == ATLAS_GRAYSCALE) { let a = textureLoad(atlas_grayscale, ic, 0).r; diff --git a/sugarloaf/src/grid/shaders/grid_text.vert.glsl b/sugarloaf/src/grid/shaders/grid_text.vert.glsl index b9517000..2e85a9d6 100644 --- a/sugarloaf/src/grid/shaders/grid_text.vert.glsl +++ b/sugarloaf/src/grid/shaders/grid_text.vert.glsl @@ -123,10 +123,14 @@ void main() { // Cursor cell color swap: if this glyph's cell is under the cursor // and it's *not* the cursor glyph itself, override with cursor_color. + // Skip when cursor_color.a == 0 — that's the hollow / unfocused + // path where the underlying glyph colour should stay intact. bool is_cursor_pos = (in_grid_pos.x == uniforms.cursor_pos.x) && (in_grid_pos.y == uniforms.cursor_pos.y); - if ((in_bools & BOOL_IS_CURSOR_GLYPH) == 0u && is_cursor_pos) { + if ((in_bools & BOOL_IS_CURSOR_GLYPH) == 0u + && is_cursor_pos + && uniforms.cursor_color.a > 0.0) { vec4 c = uniforms.cursor_color; c.rgb = grid_prepare_output_rgb(c.rgb, uniforms.input_colorspace); c.rgb *= c.a; diff --git a/sugarloaf/src/grid/vulkan.rs b/sugarloaf/src/grid/vulkan.rs index 4d6d29cb..e880d3ba 100644 --- a/sugarloaf/src/grid/vulkan.rs +++ b/sugarloaf/src/grid/vulkan.rs @@ -521,6 +521,51 @@ impl VulkanGridRenderer { self.bg_dirty = [true; FRAMES_IN_FLIGHT]; } + pub fn set_block_cursor(&mut self, cells: &[CellText]) { + if let Some(slot) = self.fg_rows.first_mut() { + if slot.is_empty() && cells.is_empty() { + return; + } + slot.clear(); + slot.extend_from_slice(cells); + self.fg_dirty = [true; FRAMES_IN_FLIGHT]; + } + } + + pub fn set_non_block_cursor(&mut self, cells: &[CellText]) { + let idx = self.fg_rows.len().saturating_sub(1); + if let Some(slot) = self.fg_rows.get_mut(idx) { + if slot.is_empty() && cells.is_empty() { + return; + } + slot.clear(); + slot.extend_from_slice(cells); + self.fg_dirty = [true; FRAMES_IN_FLIGHT]; + } + } + + pub fn clear_cursor(&mut self) { + let mut changed = false; + if let Some(slot) = self.fg_rows.first_mut() { + if !slot.is_empty() { + slot.clear(); + changed = true; + } + } + let last = self.fg_rows.len().saturating_sub(1); + if last > 0 { + if let Some(slot) = self.fg_rows.get_mut(last) { + if !slot.is_empty() { + slot.clear(); + changed = true; + } + } + } + if changed { + self.fg_dirty = [true; FRAMES_IN_FLIGHT]; + } + } + #[inline] pub fn lookup_glyph(&self, key: GlyphKey) -> Option { self.atlas_grayscale.lookup(key) diff --git a/sugarloaf/src/grid/webgpu.rs b/sugarloaf/src/grid/webgpu.rs index 96ee5670..862fe9fb 100644 --- a/sugarloaf/src/grid/webgpu.rs +++ b/sugarloaf/src/grid/webgpu.rs @@ -371,6 +371,51 @@ impl WgpuGridRenderer { self.bg_dirty = true; } + pub fn set_block_cursor(&mut self, cells: &[CellText]) { + if let Some(slot) = self.fg_rows.first_mut() { + if slot.is_empty() && cells.is_empty() { + return; + } + slot.clear(); + slot.extend_from_slice(cells); + self.fg_dirty = true; + } + } + + pub fn set_non_block_cursor(&mut self, cells: &[CellText]) { + let idx = self.fg_rows.len().saturating_sub(1); + if let Some(slot) = self.fg_rows.get_mut(idx) { + if slot.is_empty() && cells.is_empty() { + return; + } + slot.clear(); + slot.extend_from_slice(cells); + self.fg_dirty = true; + } + } + + pub fn clear_cursor(&mut self) { + let mut changed = false; + if let Some(slot) = self.fg_rows.first_mut() { + if !slot.is_empty() { + slot.clear(); + changed = true; + } + } + let last = self.fg_rows.len().saturating_sub(1); + if last > 0 { + if let Some(slot) = self.fg_rows.get_mut(last) { + if !slot.is_empty() { + slot.clear(); + changed = true; + } + } + } + if changed { + self.fg_dirty = true; + } + } + pub fn lookup_glyph(&self, key: GlyphKey) -> Option { self.atlas_grayscale.lookup(key) } @@ -627,7 +672,7 @@ fn create_text_atlas_bg( } fn premultiplied_blend() -> wgpu::BlendState { - // Premultiplied-over, matching Ghostty. Text fragment returns + // Premultiplied-over, matching Text fragment returns // premultiplied RGBA (`in.color * mask_a` for grayscale, atlas // sample for color), so source RGB must be `One`. wgpu::BlendState { diff --git a/sugarloaf/src/layout/content.rs b/sugarloaf/src/layout/content.rs index 80a19212..1afcb4c9 100644 --- a/sugarloaf/src/layout/content.rs +++ b/sugarloaf/src/layout/content.rs @@ -357,7 +357,7 @@ pub enum SpanStyleDecoration { #[derive(Copy, Clone, PartialEq, Debug)] pub struct SpanStyle { pub font_id: usize, - // Unicode width + // Unicode width pub width: f32, /// Font attributes. pub font_attrs: Attributes, @@ -549,14 +549,14 @@ impl Content { // Cell width = max advance across all printable ASCII, // queried on a CTFont clone at the real render size (not // the 1pt base — that returns bogus 1.0-per-glyph - // advances on some fonts). Mirrors Ghostty - // (`coretext.zig:773-804`). Progressive fallbacks: - // 1. max-ASCII at this size (right answer on every - // real font we've seen) - // 2. advance of space (pre-existing behaviour; may - // return None) - // 3. `font_size` itself (the em — last-resort, wider - // than any real monospace advance) + // advances on some fonts). Mirrors + //. Progressive fallbacks: + // 1. max-ASCII at this size (right answer on every + // real font we've seen) + // 2. advance of space (pre-existing behaviour; may + // return None) + // 3. `font_size` itself (the em — last-resort, wider + // than any real monospace advance) let char_width = crate::font::macos::max_ascii_advance_px(&handle, font_size) .or_else(|| { crate::font::macos::advance_units_for_char(&handle, ' ') @@ -627,8 +627,8 @@ impl Content { scale: layout.dimensions.scale, }; - // println!(" -> Returning dimensions (physical): width={}, height={}, scale={}", - // result.width, result.height, result.scale); + // println!(" -> Returning dimensions (physical): width={}, height={}, scale={}", + // result.width, result.height, result.scale); return result; } @@ -1416,7 +1416,7 @@ impl Content { } } -/// Run-level shaping cache (like Ghostty's ShaperCache). +/// Run-level shaping cache (like ShaperCache). /// /// Caches pre-packed shaped runs per text run, keyed by (content + font_id). /// The shaper always sees the full run so ligatures are handled naturally. diff --git a/sugarloaf/src/renderer/image.metal b/sugarloaf/src/renderer/image.metal index 3e9a5c16..04632b33 100644 --- a/sugarloaf/src/renderer/image.metal +++ b/sugarloaf/src/renderer/image.metal @@ -11,11 +11,11 @@ struct Globals { }; struct ImageInstanceInput { - // Screen position of the image top-left (physical pixels). + // Screen position of the image top-left (physical pixels). float2 dest_pos [[attribute(0)]]; - // Size of the image on screen (physical pixels). + // Size of the image on screen (physical pixels). float2 dest_size [[attribute(1)]]; - // Source rectangle: xy = origin, zw = size (normalized 0..1). + // Source rectangle: xy = origin, zw = size (normalized 0..1). float4 source_rect [[attribute(2)]]; }; @@ -29,18 +29,18 @@ vertex ImageVertexOut image_vs_main( ImageInstanceInput instance [[stage_in]], constant Globals &globals [[buffer(1)]] ) { - // Triangle strip: 4 vertices → quad - // 0 → 1 - // | /| - // 2 → 3 + // Triangle strip: 4 vertices → quad + // 0 → 1 + // | /| + // 2 → 3 float2 corner; corner.x = float(vid == 1 || vid == 3); corner.y = float(vid == 2 || vid == 3); - // `source_rect` is `[u0, v0, u1, v1]` (origin, end), not (origin, size). - // `mix(a, b, t)` computes `a + (b-a)*t`, so corner=(0,0) → (u0,v0) - // and corner=(1,1) → (u1,v1). The previous `xy + zw * corner` form - // only worked when `xy == [0,0]` (the full-image default). + // `source_rect` is `[u0, v0, u1, v1]` (origin, end), not (origin, size). + // `mix(a, b, t)` computes `a + (b-a)*t`, so corner=(0,0) → (u0,v0) + // and corner=(1,1) → (u1,v1). The previous `xy + zw * corner` form + // only worked when `xy == [0,0]` (the full-image default). float2 tex_coord = mix(instance.source_rect.xy, instance.source_rect.zw, corner); float2 image_pos = instance.dest_pos + instance.dest_size * corner; @@ -62,7 +62,7 @@ vertex ImageVertexOut image_vs_main( // matching ghostty's punchier emoji / sixel / kitty-graphics look. // // Rec.2020 still gets a matrix because its primaries diverge enough from -// P3 that "treat as P3 directly" would clip badly. Ghostty has no +// P3 that "treat as P3 directly" would clip badly. has no // Rec.2020 image path, so we own this decision. static inline float3 linear_to_srgb(float3 c) { float3 lo = c * 12.92; @@ -84,14 +84,14 @@ fragment float4 image_fs_main( texture2d image_texture [[texture(0)]], sampler image_sampler [[sampler(0)]] ) { - // Sample returns linear RGBA (HW sRGB-decoded); alpha is linear by - // convention, untouched by the format's transfer curve. + // Sample returns linear RGBA (HW sRGB-decoded); alpha is linear by + // convention, untouched by the format's transfer curve. float4 rgba = image_texture.sample(image_sampler, input.tex_coord); float3 lin = rgba.rgb; if (globals.input_colorspace == 2u) { lin = rec2020_to_p3(lin); } float3 enc = linear_to_srgb(lin); - // Premultiply alpha (pipeline blend factors are One / OneMinusSrcAlpha). + // Premultiply alpha (pipeline blend factors are One / OneMinusSrcAlpha). return float4(enc * rgba.a, rgba.a); } diff --git a/sugarloaf/src/renderer/mod.rs b/sugarloaf/src/renderer/mod.rs index 53c9f9b9..10032221 100644 --- a/sugarloaf/src/renderer/mod.rs +++ b/sugarloaf/src/renderer/mod.rs @@ -97,7 +97,7 @@ pub struct WgpuRenderer { /// them to the DisplayP3-tagged framebuffer: /// - `0` = sRGB. Apply the sRGB → DisplayP3 primaries matrix after /// linearization so `#ff0000` displays as the sRGB-standard red rather than -/// P3-pure red. Matches ghostty's default. +/// P3-pure red. /// - `1` = DisplayP3. Treat inputs as already-P3, skip the matrix. /// - `2` = Rec.2020. Skipped (matrix pending), matches `1` in practice. /// @@ -1200,15 +1200,15 @@ impl Renderer { // Useful for debug occasionally // let inst_bytes = - // self.instances.len() * std::mem::size_of::(); + // self.instances.len() * std::mem::size_of::(); // let vert_bytes = self.vertices.len() * std::mem::size_of::(); // println!( - // "gpu upload: {} instances ({:.2} MB) + {} verts ({:.2} MB) = {:.2} MB", - // self.instances.len(), - // inst_bytes as f64 / (1024.0 * 1024.0), - // self.vertices.len(), - // vert_bytes as f64 / (1024.0 * 1024.0), - // (inst_bytes + vert_bytes) as f64 / (1024.0 * 1024.0), + // "gpu upload: {} instances ({:.2} MB) + {} verts ({:.2} MB) = {:.2} MB", + // self.instances.len(), + // inst_bytes as f64 / (1024.0 * 1024.0), + // self.vertices.len(), + // vert_bytes as f64 / (1024.0 * 1024.0), + // (inst_bytes + vert_bytes) as f64 / (1024.0 * 1024.0), // ); } @@ -2377,16 +2377,16 @@ impl Renderer { /// Record sugarloaf's own draws inside the active dynamic-rendering /// pass that `Sugarloaf::render_vulkan` opens. Order: - /// 1. Background image (full-screen quad). - /// 2. BelowText image overlays (kitty / sixel placements with - /// `dest_pos.z < 0`). - /// 3. Rich-text quad pass — `quad()` / `rect()` calls + cell - /// underline decorations (dashed/dotted/curly handled in - /// `quad.frag.glsl`). - /// 4. Non-quad geometry — `polygon()` / `line()` / `triangle()` - /// / `arc()` calls (cursor underline shape, hint highlights). - /// 5. AboveText image overlays. - /// 6. Optional bootstrap rect (`RIO_VULKAN_BOOTSTRAP=1`). + /// 1. Background image (full-screen quad). + /// 2. BelowText image overlays (kitty / sixel placements with + /// `dest_pos.z < 0`). + /// 3. Rich-text quad pass — `quad()` / `rect()` calls + cell + /// underline decorations (dashed/dotted/curly handled in + /// `quad.frag.glsl`). + /// 4. Non-quad geometry — `polygon()` / `line()` / `triangle()` + /// / `arc()` calls (cursor underline shape, hint highlights). + /// 5. AboveText image overlays. + /// 6. Optional bootstrap rect (`RIO_VULKAN_BOOTSTRAP=1`). /// /// Glyph atlas sampling through this pipeline isn't ported — /// grid text + UI text overlay each own dedicated atlas diff --git a/sugarloaf/src/renderer/renderer.metal b/sugarloaf/src/renderer/renderer.metal index 8cdef51f..cae22a4c 100644 --- a/sugarloaf/src/renderer/renderer.metal +++ b/sugarloaf/src/renderer/renderer.metal @@ -157,9 +157,9 @@ float3 rec2020_to_p3(float3 linear_r2020) { // DisplayP3 — stores gamma-encoded values that the compositor can // display directly, and our alpha blending stays in gamma space // (matches ghostty `alpha-blending = native`). -// 0 = sRGB → sRGB → P3 matrix -// 1 = DisplayP3 → identity (already P3) -// 2 = Rec.2020 → Rec.2020 → P3 matrix +// 0 = sRGB → sRGB → P3 matrix +// 1 = DisplayP3 → identity (already P3) +// 2 = Rec.2020 → Rec.2020 → P3 matrix float3 prepare_output_rgb(float3 srgb, uchar input_colorspace) { float3 lin = srgb_to_linear(srgb); if (input_colorspace == 0u) { @@ -190,15 +190,15 @@ float pick_corner_radius(float2 center_to_point, float4 corner_radii) { // Signed distance field for a quad (rectangle) float quad_sdf(float2 corner_center_to_point, float corner_radius) { if (corner_radius == 0.0) { - // Fast path for sharp corners + // Fast path for sharp corners return max(corner_center_to_point.x, corner_center_to_point.y); } else { - // Signed distance of the point from a quad that is inset by corner_radius. - // It is negative inside this quad, and positive outside. + // Signed distance of the point from a quad that is inset by corner_radius. + // It is negative inside this quad, and positive outside. float signed_distance_to_inset_quad = - // 0 inside the inset quad, and positive outside. + // 0 inside the inset quad, and positive outside. length(max(float2(0.0), corner_center_to_point)) + - // 0 outside the inset quad, and negative inside. + // 0 outside the inset quad, and negative inside. min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); return signed_distance_to_inset_quad - corner_radius; } @@ -213,12 +213,12 @@ float fmod_pos(float a, float b) { // Calculate underline alpha for pattern rendering float underline_alpha(float x_pos, float y_pos, float rect_height, float thickness, int style) { - // style 1: regular solid line + // style 1: regular solid line if (style == 1) { return 1.0; } - // style 2: dashed (6px dash, 2px gap) + // style 2: dashed (6px dash, 2px gap) if (style == 2) { float antialias = 0.5; float dash_width = 6.0; @@ -230,7 +230,7 @@ float underline_alpha(float x_pos, float y_pos, float rect_height, float thickne return min(start_aa, end_aa); } - // style 3: dotted (2px dot, 2px gap) + // style 3: dotted (2px dot, 2px gap) if (style == 3) { float antialias = 0.5; float dot_width = 2.0; @@ -242,7 +242,7 @@ float underline_alpha(float x_pos, float y_pos, float rect_height, float thickne return min(start_aa, end_aa); } - // style 4: curly (sine wave) using SDF + // style 4: curly (sine wave) using SDF if (style == 4) { const float WAVE_FREQUENCY = 2.0; const float WAVE_HEIGHT_RATIO = 0.8; @@ -283,8 +283,8 @@ fragment float4 fs_main( float4 out = input.f_color; - // Handle GPU-rendered underlines - // Underlines have: underline_style > 0, thickness in corner_radii.x + // Handle GPU-rendered underlines + // Underlines have: underline_style > 0, thickness in corner_radii.x if (input.underline_style > 0) { float width = input.rect_size.x; float rect_height = input.rect_size.y; @@ -299,7 +299,7 @@ fragment float4 fs_main( ); } - // Handle texture sampling for glyphs + // Handle texture sampling for glyphs if (input.color_layer > 0) { out = color_texture.sample(font_sampler, input.f_uv, level(0.0)); } @@ -309,11 +309,11 @@ fragment float4 fs_main( out = float4(out.xyz, input.f_color.a * mask_alpha); } - // Check if we have any rounding + // Check if we have any rounding bool has_corners = input.corner_radii.x != 0.0 || input.corner_radii.y != 0.0 || input.corner_radii.z != 0.0 || input.corner_radii.w != 0.0; - // Fast path: no rounding + // Fast path: no rounding if (!has_corners) { return float4(prepare_output_rgb(out.rgb, globals.input_colorspace), out.a); } @@ -321,25 +321,25 @@ fragment float4 fs_main( float2 size = input.rect_size; float2 half_size = size / 2.0; - // Convert UV (0-1) to local position centered at rect center + // Convert UV (0-1) to local position centered at rect center float2 center_to_point = (input.f_uv - 0.5) * size; - // Antialiasing threshold + // Antialiasing threshold float antialias_threshold = 0.5; - // Pick the corner radius for this quadrant + // Pick the corner radius for this quadrant float corner_radius = pick_corner_radius(center_to_point, input.corner_radii); - // Vector from corner to point (mirrored to bottom-right quadrant) + // Vector from corner to point (mirrored to bottom-right quadrant) float2 corner_to_point = abs(center_to_point) - half_size; - // Vector from corner center (for rounded corner) to point + // Vector from corner center (for rounded corner) to point float2 corner_center_to_point = corner_to_point + corner_radius; - // Outer SDF: distance to the outer edge of the quad + // Outer SDF: distance to the outer edge of the quad float outer_sdf = quad_sdf(corner_center_to_point, corner_radius); - // If outside the quad, discard + // If outside the quad, discard if (outer_sdf >= antialias_threshold) { discard_fragment(); } -- 2.51.2