Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Editor

Group: Content

Code

const LINE_LEN: usize = 500;
/// CSS/Pango generic-family token — see [`quadraui::GenericFamily`] — not
/// a concrete font name, so this resolves to whatever monospace face
/// each backend's host actually has installed instead of assuming a
/// fontconfig-only name like `"DejaVu Sans Mono"` exists everywhere.
const DEMO_FONT_FAMILY: &str = "monospace";
const DEMO_FONT_SIZE_PT: f32 = 24.0;

fn plain_line(raw_text: String, gutter: &str, idx: usize) -> EditorLine {
    let fg = Color::rgb(220, 220, 220);
    // `EditorStyledSpan::{start_byte,end_byte}` are byte offsets (see
    // that struct's own field doc), so `raw_text.len()` — the string's
    // byte length, not a char count — is the correct span end for any
    // text, ASCII or not.
    let end_byte = raw_text.len();
    EditorLine {
        raw_text,
        gutter_text: gutter.to_string(),
        spans: vec![EditorStyledSpan {
            start_byte: 0,
            end_byte,
            style: EditorStyle {
                fg,
                bg: None,
                bold: false,
                italic: false,
                font_scale: 1.0,
            },
        }],
        line_idx: idx,
        is_current_line: idx == 0,
        is_fold_header: false,
        folded_line_count: 0,
        git_diff: None,
        diff_status: None,
        diagnostics: vec![],
        spell_errors: vec![],
        is_breakpoint: false,
        is_conditional_bp: false,
        is_dap_current: false,
        is_wrap_continuation: false,
        segment_col_offset: 0,
        annotation: None,
        ghost_suffix: None,
        is_ghost_continuation: false,
        indent_guides: vec![],
        colorcolumns: vec![],
    }
}

/// Variant 0 state: horizontal-scroll smoke test.
struct HScrollState {
    cursor_col: usize,
    scroll_left: usize,
}

impl HScrollState {
    fn new() -> Self {
        Self {
            cursor_col: 0,
            scroll_left: 0,
        }
    }

    fn line_text() -> String {
        (0..LINE_LEN)
            .map(|i| ((i % 10) as u8 + b'0') as char)
            .collect()
    }

    fn viewport_cols(area: Rect, backend: &dyn Backend) -> usize {
        let cw = backend.char_width();
        let gutter_w = 4.0 * cw;
        ((area.width - gutter_w) / cw).floor().max(1.0) as usize
    }

    fn ensure_cursor_visible(&mut self, viewport_cols: usize) {
        if self.cursor_col < self.scroll_left {
            self.scroll_left = self.cursor_col;
        } else if self.cursor_col >= self.scroll_left + viewport_cols {
            self.scroll_left = self.cursor_col + 1 - viewport_cols;
        }
    }

    fn build_editor(&self, area: Rect) -> Editor {
        let line = plain_line(Self::line_text(), "   1", 0);
        Editor::new(WidgetId::new("gallery:editor:hscroll"), area)
            .with_lines(vec![line])
            .with_cursor(EditorCursor {
                pos: EditorCursorPos {
                    view_line: 0,
                    col: self.cursor_col,
                },
                shape: EditorCursorShape::Block,
            })
            .with_scroll_left(self.scroll_left)
            .with_total_lines(1)
            .with_max_col(LINE_LEN)
            .with_gutter_char_width(4)
            .with_is_active(true)
            .with_cursorline(true)
            .with_lightbulb_glyph('\0')
    }
}

/// Font this demo restores on variant 0 once it has applied the
/// override at least once — the shell runner's own documented default
/// (`ShellConfig`'s doc: GTK falls back to `"Monospace 11"` when no
/// `with_editor_font` override is configured), expressed as the generic
/// token so every backend resolves it to its own native monospace face
/// rather than this demo hardcoding one.
const DEFAULT_FONT_FAMILY: &str = "monospace";
const DEFAULT_FONT_SIZE_PT: f32 = 11.0;

/// Variant 1 state: font-override smoke test. `applied` tracks the
/// `(family, size_pt)` pair actually last pushed to the backend, so
/// repeated renders of the same variant don't re-issue `set_editor_font`
/// every frame — mirroring the `Caret Shape` demo's
/// `Cell<Option<EditorCursorShape>>` debounce pattern, including its
/// shape: `None` until this demo's font is touched for the first time,
/// so a session that never visits "Font override" never calls
/// `set_editor_font` at all.
struct FontState {
    applied: Cell<Option<(&'static str, f32)>>,
    calls: Cell<u32>,
}

impl FontState {
    fn new() -> Self {
        Self {
            applied: Cell::new(None),
            calls: Cell::new(0),
        }
    }

    /// Re-applies the override (debounced) and counts the call.
    fn apply_override(&self, backend: &mut dyn Backend) {
        let desired = (DEMO_FONT_FAMILY, DEMO_FONT_SIZE_PT);
        if self.applied.get() != Some(desired) {
            backend.set_editor_font(desired.0, desired.1);
            self.applied.set(Some(desired));
            self.calls.set(self.calls.get() + 1);
        }
    }

    /// Puts the default font back, but only if this demo had actually
    /// overridden it — leaving a session that never visited "Font
    /// override" untouched, and not counting the restore in `calls`
    /// (that counter answers "how many times did the override fire",
    /// not "how many times was the font touched at all").
    ///
    /// Only reaches the backend while this `EditorDemo` instance is
    /// itself the one rendering: the gallery's `Demo` trait has no
    /// "leaving this demo" hook, so switching straight from "Font
    /// override" to another demo (Diff View, Minimap, Caret Shape, …)
    /// still leaves the 24pt override live there until this demo's own
    /// variant 0 renders again.
    fn restore_default_if_overridden(&self, backend: &mut dyn Backend) {
        let default = (DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE_PT);
        if matches!(self.applied.get(), Some(current) if current != default) {
            backend.set_editor_font(default.0, default.1);
            self.applied.set(Some(default));
        }
    }

    fn build_editor(area: Rect) -> Editor {
        let texts = [
            "The quick brown fox jumps over the lazy dog",
            "set_editor_font() painted this at 24pt \"monospace\"",
        ];
        let lines = texts
            .iter()
            .enumerate()
            .map(|(idx, text)| plain_line(text.to_string(), &format!("{:>4}", idx + 1), idx))
            .collect();
        Editor::new(WidgetId::new("gallery:editor:font"), area)
            .with_lines(lines)
            .with_cursor(EditorCursor {
                pos: EditorCursorPos {
                    view_line: 0,
                    col: 0,
                },
                shape: EditorCursorShape::Block,
            })
            .with_total_lines(texts.len())
            .with_gutter_char_width(4)
            .with_is_active(true)
            .with_cursorline(true)
            .with_lightbulb_glyph('\0')
    }
}

pub struct EditorDemo {
    hscroll: HScrollState,
    font: FontState,
}

impl EditorDemo {
    pub fn new() -> Self {
        Self {
            hscroll: HScrollState::new(),
            font: FontState::new(),
        }
    }
}

impl Default for EditorDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for EditorDemo {
    fn name(&self) -> &'static str {
        "Editor"
    }

    fn group(&self) -> &'static str {
        "Content"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["Horizontal scroll", "Font override"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let lh = backend.line_height();
        let bar_h = if lh > 1.5 { lh * 1.5 } else { lh };
        let editor_area = Rect::new(area.x, area.y, area.width, (area.height - bar_h).max(0.0));
        let bar_rect = Rect::new(area.x, area.y + editor_area.height, area.width, bar_h);

        let status = match variant {
            1 => {
                self.font.apply_override(backend);
                let editor = FontState::build_editor(editor_area);
                backend.draw_editor(editor.rect, &editor);
                format!(
                    " editor font: {DEMO_FONT_FAMILY} {DEMO_FONT_SIZE_PT}pt  calls: {} ",
                    self.font.calls.get()
                )
            }
            _ => {
                self.font.restore_default_if_overridden(backend);
                let editor = self.hscroll.build_editor(editor_area);
                backend.draw_editor(editor.rect, &editor);
                let vpc = HScrollState::viewport_cols(editor_area, backend);
                format!(
                    " col {} / {}  scroll_left {}  viewport_cols {} ",
                    self.hscroll.cursor_col + 1,
                    LINE_LEN,
                    self.hscroll.scroll_left,
                    vpc
                )
            }
        };

        let bar = StatusBar {
            id: WidgetId::new("gallery:editor:status"),
            left_segments: vec![StatusBarSegment {
                text: status,
                fg: Color::rgb(255, 255, 255),
                bg: Color::rgb(40, 80, 120),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![],
        };
        let _ = backend.draw_status_bar_interactive(bar_rect, &bar, &InteractionState::new());
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        if variant != 0 {
            return Reaction::Continue;
        }
        let vpc = HScrollState::viewport_cols(area, backend);
        match event {
            UiEvent::KeyPressed { key, .. } => {
                match key {
                    Key::Char('$') | Key::Named(NamedKey::End) => {
                        self.hscroll.cursor_col = LINE_LEN - 1;
                    }
                    Key::Char('0') | Key::Named(NamedKey::Home) => {
                        self.hscroll.cursor_col = 0;
                    }
                    Key::Char('l') | Key::Named(NamedKey::Right) => {
                        if self.hscroll.cursor_col < LINE_LEN - 1 {
                            self.hscroll.cursor_col += 1;
                        }
                    }
                    Key::Char('h') | Key::Named(NamedKey::Left) => {
                        self.hscroll.cursor_col = self.hscroll.cursor_col.saturating_sub(1);
                    }
                    _ => return Reaction::Continue,
                }
                self.hscroll.ensure_cursor_visible(vpc);
                Reaction::Redraw
            }
            _ => Reaction::Continue,
        }
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        match variant {
            1 => serde_json::json!({
                "font_family": DEMO_FONT_FAMILY,
                "font_size_pt": DEMO_FONT_SIZE_PT,
                "set_editor_font_calls": self.font.calls.get(),
            }),
            _ => serde_json::json!({
                "cursor_col": self.hscroll.cursor_col,
                "scroll_left": self.hscroll.scroll_left,
                "line_len": LINE_LEN,
            }),
        }
    }

    fn caps_note(&self, variant: usize, caps: &BackendCaps) -> Option<String> {
        if variant == 1 && !caps.generic_font_families {
            Some(
                "This backend has a fixed cell grid — set_editor_font is a structural \
                 no-op here; the font override has no visible effect."
                    .to_string(),
            )
        } else {
            None
        }
    }
}

Backends

VariantTUIGTKmacOSWindows
Horizontal scroll
Font override