Skip to content

RichTextPopup primitive: editor hover with selection + scroll + clickable links + tree-sitter code highlight #214

Description

@JDonaghy

Goal

Build a `quadraui::RichTextPopup` primitive that lets both TUI and GTK render the LSP/editor hover popup from one shared codepath. Today the hover popup is the largest remaining bespoke-renderer surface in the chrome migration (#205 deferred slice 4).

What it needs to support

The existing TUI `render_editor_hover_popup` (`src/tui_main/panels.rs:2714`) and GTK `draw_editor_hover_popup` (`src/gtk/draw.rs:1860`) collectively need:

Feature Used by
Multi-line styled text rendered inside a bordered box LSP hover, error popups
Markdown styling (headings, bold/italic, code spans, links) LSP hover for documented APIs
Fenced code blocks with tree-sitter syntax highlighting LSP hover for Rust/etc. examples
Focus state (border colour changes when popup has keyboard focus) Hover with K, click-to-pin
Vertical scroll + scrollbar Long doc strings
Text selection (drag-select chars, multi-line) Copy text out of the popup
Clickable links with focus-cycle navigation (Tab) LSP hover with hyperlinked types
Auto-place above-or-below anchor Position relative to cursor

Why not just extend Tooltip

Tooltip is "static text in a box". The editor hover is "an interactive document viewer". Conflating them leaves Tooltip's API messy for everyone else (signature help, diff peek, simple LSP hovers) without buying much. A separate primitive captures the "this surface is interactive and stateful" distinction at the type level.

Suggested shape

```rust
pub struct RichTextPopup {
pub id: WidgetId,
pub rendered: RichTextDocument, // pre-laid-out lines + spans + code-highlight info
pub scroll_top: usize,
pub has_focus: bool,
pub selection: Option,
pub links: Vec,
pub focused_link: Option,
pub placement: TooltipPlacement,
}

pub struct RichTextPopupLayout {
pub bounds: Rect,
pub visible_lines: Vec,
pub scrollbar: Option,
pub link_hit_regions: Vec<(Rect, usize)>, // for click-to-open
pub char_hit_regions: Vec<(Rect, usize, usize)>, // (line, col) for selection
pub hit_regions: Vec<(Rect, RichTextPopupHit)>,
}

pub enum RichTextPopupEvent {
LinkClicked { idx: usize },
LinkFocused { idx: usize },
SelectionChanged(Option),
Scrolled { new_offset: usize },
Closed,
KeyPressed { key, modifiers },
}
```

The `RichTextDocument` shape already exists in vimcode (`core::markdown::Rendered` or similar) — it would move to or be referenced from quadraui.

Migration order (suggested)

  1. Define the primitive + adapters from `render::EditorHoverPopupData` → `RichTextPopup`.
  2. Build `quadraui_tui::draw_rich_text_popup` rasteriser. Migrate TUI `render_editor_hover_popup` to consume it.
  3. Build `quadraui_gtk::draw_rich_text_popup` rasteriser. Migrate GTK `draw_editor_hover_popup` to consume it.
  4. Wire scroll/click/select/link events through `dispatch_mouse_*` per ContextMenu: motion handlers should use primitive's hit_test, not hand-rolled row math #210's pattern so motion handlers don't drift.

Closes

Closes #205 slice 4 (deferred). Eliminates the largest remaining bespoke chrome renderer in both backends.

Rough sizing

  • New primitive: ~400 LOC (incl tests)
  • Adapter: ~80 LOC
  • TUI rasteriser: ~200 LOC (replaces a ~250-LOC bespoke renderer)
  • GTK rasteriser: ~250 LOC (replaces a ~330-LOC bespoke renderer)
  • Glue / event routing: ~150 LOC
  • Total ~1100 LOC added, ~580 deleted

Bigger than any individual slice on #205. Worth its own focused wave.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    infrastructureBuild, CI, distributionuiUI/rendering

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions