diff --git a/render_state_cell.go b/render_state_cell.go index 9493be6..dc2ade6 100644 --- a/render_state_cell.go +++ b/render_state_cell.go @@ -5,6 +5,83 @@ package libghostty /* #include + +// render_cell_style_snapshot is a small binding-side adapter over the current +// libghostty render-cell APIs. The upstream API exposes each style/color field +// separately, which is flexible but expensive for renderers because each Go +// getter is a cgo transition and the allocation-returning Go getters materialize +// *Style / *ColorRGB values. This helper keeps the same libghostty behavior but +// batches the fields into one cgo call and a caller-owned result struct. +typedef struct { + bool has_styling; + bool has_foreground; + GhosttyColorRgb foreground; + bool has_background; + GhosttyColorRgb background; + bool bold; + bool faint; + bool italic; + bool underline; + bool strikethrough; + bool inverse; +} render_cell_style_snapshot; + +static inline GhosttyResult render_cell_style_snapshot_get( + GhosttyRenderStateRowCells cells, + render_cell_style_snapshot* out +) { + if (out == NULL) return GHOSTTY_INVALID_VALUE; + + *out = (render_cell_style_snapshot){0}; + + GhosttyResult result = ghostty_render_state_row_cells_get( + cells, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING, + &out->has_styling + ); + if (result != GHOSTTY_SUCCESS) return result; + + if (out->has_styling) { + GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle); + result = ghostty_render_state_row_cells_get( + cells, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, + &style + ); + if (result != GHOSTTY_SUCCESS) return result; + + out->bold = style.bold; + out->faint = style.faint; + out->italic = style.italic; + out->underline = style.underline != GHOSTTY_SGR_UNDERLINE_NONE; + out->strikethrough = style.strikethrough; + out->inverse = style.inverse; + } + + result = ghostty_render_state_row_cells_get( + cells, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, + &out->foreground + ); + if (result == GHOSTTY_SUCCESS) { + out->has_foreground = true; + } else if (result != GHOSTTY_INVALID_VALUE) { + return result; + } + + result = ghostty_render_state_row_cells_get( + cells, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, + &out->background + ); + if (result == GHOSTTY_SUCCESS) { + out->has_background = true; + } else if (result != GHOSTTY_INVALID_VALUE) { + return result; + } + + return GHOSTTY_SUCCESS; +} */ import "C" @@ -55,6 +132,50 @@ const ( RenderStateRowCellsDataHasStyling RenderStateRowCellsData = C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING ) +// RenderCellStyle is a reusable, resolved style snapshot for the current +// render-state cell. It corresponds to the data returned by +// ghostty_render_state_row_cells_get for style, foreground color, background +// color, and has-styling, flattened into one Go value for hot render paths. +// +// Foreground and Background point at storage owned by this struct when set and +// are nil when the cell has no explicit/resolved color. Because those pointers +// target internal fields, callers should not copy a RenderCellStyle value after +// calling [RenderStateRowCells.ResolvedStyleInto]; pass the same value by +// pointer and reuse it instead. +type RenderCellStyle struct { + // Foreground is the resolved foreground color, or nil when the cell has no + // explicit foreground and the renderer should use its default foreground. + Foreground *ColorRGB + + // Background is the resolved background color, or nil when the cell has no + // explicit background and the renderer should use its default background. + Background *ColorRGB + + // HasStyling reports whether the cell has any explicit non-default styling. + HasStyling bool + + // Bold reports whether bold text is set. + Bold bool + + // Faint reports whether faint/dim text is set. + Faint bool + + // Italic reports whether italic text is set. + Italic bool + + // Underline reports whether any underline style is set. + Underline bool + + // Strikethrough reports whether strikethrough text is set. + Strikethrough bool + + // Inverse reports whether inverse video is set. + Inverse bool + + foreground ColorRGB + background ColorRGB +} + // RenderStateRowCells iterates over cells in a render-state row. // Create one with NewRenderStateRowCells, populate it via // [RenderStateRowIterator.Cells], then advance with @@ -161,6 +282,52 @@ func (rc *RenderStateRowCells) Style() (*Style, error) { return &Style{c: cs}, nil } +// StyleInto fills dst with the reusable resolved style snapshot for the current +// cell. It is an alias for [RenderStateRowCells.ResolvedStyleInto]. +func (rc *RenderStateRowCells) StyleInto(dst *RenderCellStyle) error { + return rc.ResolvedStyleInto(dst) +} + +// ResolvedStyleInto fills dst with the current cell's resolved colors and text +// style flags. This is the allocation-reusing form of querying Style, FgColor, +// BgColor, and HasStyling separately: it performs one cgo transition, stores +// colors in dst-owned memory, and sets Foreground/Background to nil when the +// corresponding color is absent. +func (rc *RenderStateRowCells) ResolvedStyleInto(dst *RenderCellStyle) error { + if dst == nil { + return errors.New("libghostty: nil RenderCellStyle") + } + + var snap C.render_cell_style_snapshot + if err := resultError(C.render_cell_style_snapshot_get(rc.ptr, &snap)); err != nil { + return err + } + + dst.HasStyling = bool(snap.has_styling) + dst.Bold = bool(snap.bold) + dst.Faint = bool(snap.faint) + dst.Italic = bool(snap.italic) + dst.Underline = bool(snap.underline) + dst.Strikethrough = bool(snap.strikethrough) + dst.Inverse = bool(snap.inverse) + + if bool(snap.has_foreground) { + dst.foreground = ColorRGB{R: uint8(snap.foreground.r), G: uint8(snap.foreground.g), B: uint8(snap.foreground.b)} + dst.Foreground = &dst.foreground + } else { + dst.Foreground = nil + } + + if bool(snap.has_background) { + dst.background = ColorRGB{R: uint8(snap.background.r), G: uint8(snap.background.g), B: uint8(snap.background.b)} + dst.Background = &dst.background + } else { + dst.Background = nil + } + + return nil +} + // Graphemes returns the full grapheme cluster codepoints for the // current cell. The base codepoint is first, followed by any extra // codepoints. Returns nil if the cell has no text. diff --git a/render_state_cell_test.go b/render_state_cell_test.go index f8b9e70..96b9b70 100644 --- a/render_state_cell_test.go +++ b/render_state_cell_test.go @@ -336,6 +336,108 @@ func TestRenderStateRowCellsStyle(t *testing.T) { } } +func TestRenderStateRowCellsResolvedStyleInto(t *testing.T) { + term, err := NewTerminal(WithSize(80, 24)) + if err != nil { + t.Fatal(err) + } + defer term.Close() + + // Write one heavily styled cell and one default-styled cell. The styled + // cell exercises the resolved RGB color path as well as the style flags Rex + // needs to produce ANSI output without allocating per-cell Style/Color values. + term.VTWrite([]byte("\x1b[1;2;3;4;7;9;38;2;255;0;0;48;2;0;0;255mX\x1b[0mY")) + + rs, err := NewRenderState() + if err != nil { + t.Fatal(err) + } + defer rs.Close() + + if err := rs.Update(term); err != nil { + t.Fatal(err) + } + + ri, err := NewRenderStateRowIterator() + if err != nil { + t.Fatal(err) + } + defer ri.Close() + + if err := rs.RowIterator(ri); err != nil { + t.Fatal(err) + } + + if !ri.Next() { + t.Fatal("expected at least one row") + } + + rc, err := NewRenderStateRowCells() + if err != nil { + t.Fatal(err) + } + defer rc.Close() + + if err := ri.Cells(rc); err != nil { + t.Fatal(err) + } + + if !rc.Next() { + t.Fatal("expected at least one cell") + } + + var style RenderCellStyle + if err := rc.ResolvedStyleInto(&style); err != nil { + t.Fatal(err) + } + if !style.HasStyling { + t.Fatal("expected first cell to have styling") + } + if style.Foreground == nil || *style.Foreground != (ColorRGB{R: 255, G: 0, B: 0}) { + t.Fatalf("expected red foreground, got %#v", style.Foreground) + } + if style.Background == nil || *style.Background != (ColorRGB{R: 0, G: 0, B: 255}) { + t.Fatalf("expected blue background, got %#v", style.Background) + } + if !style.Bold || !style.Faint || !style.Italic || !style.Underline || !style.Strikethrough || !style.Inverse { + t.Fatalf("expected all style flags to be set, got %+v", style) + } + fgPtr := style.Foreground + bgPtr := style.Background + + if !rc.Next() { + t.Fatal("expected second cell") + } + if err := rc.ResolvedStyleInto(&style); err != nil { + t.Fatal(err) + } + if style.HasStyling { + t.Fatal("expected second cell to have default styling") + } + if style.Foreground != nil { + t.Fatalf("expected nil foreground for default-styled cell, got %+v", *style.Foreground) + } + if style.Background != nil { + t.Fatalf("expected nil background for default-styled cell, got %+v", *style.Background) + } + if style.Bold || style.Faint || style.Italic || style.Underline || style.Strikethrough || style.Inverse { + t.Fatalf("expected all style flags to be cleared, got %+v", style) + } + + if err := rc.Select(0); err != nil { + t.Fatal(err) + } + if err := rc.StyleInto(&style); err != nil { + t.Fatal(err) + } + if style.Foreground != fgPtr { + t.Fatal("expected foreground pointer storage to be reused") + } + if style.Background != bgPtr { + t.Fatal("expected background pointer storage to be reused") + } +} + func TestRenderStateRowCellsSelected(t *testing.T) { term, err := NewTerminal(WithSize(80, 24)) if err != nil {