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

Text Selection

Group: Content

Code

const PANGRAM_LINES: &[&str] = &[
    "The quick brown fox jumps over the lazy dog.",
    "Pack my box with five dozen liquor jugs.",
    "How vexingly quick daft zebras jump!",
    "The five boxing wizards jump quickly.",
];

const HAIKU_LINES: &[&str] = &[
    "An old silent pond",
    "A frog jumps into the pond—",
    "Splash! Silence again.",
];

const CONTENT_ID: &str = "gallery:text-selection:content";

pub struct TextSelectionDemo {
    status: String,
}

impl TextSelectionDemo {
    pub fn new() -> Self {
        Self { status: hint() }
    }

    fn lines(variant: usize) -> &'static [&'static str] {
        if variant == 1 {
            HAIKU_LINES
        } else {
            PANGRAM_LINES
        }
    }

    fn content_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0))
    }

    fn status_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y + area.height - lh, area.width, lh)
    }
}

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

impl Demo for TextSelectionDemo {
    fn name(&self) -> &'static str {
        "Text Selection"
    }

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

    fn variants(&self) -> &'static [&'static str] {
        &["Pangrams", "Haiku"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let content = Self::content_rect(area, backend);
        let lh = backend.line_height();
        let lines = Self::lines(variant);
        let fg = Color::rgb(200, 200, 200);
        let bg = Color::rgb(20, 20, 35);
        let mut rendered_rows = 0usize;
        for (i, &line) in lines.iter().enumerate() {
            let row_y = content.y + i as f32 * lh;
            if row_y + lh > content.y + content.height {
                break;
            }
            let bar = StatusBar {
                id: WidgetId::new(format!("{CONTENT_ID}-row-{i}")),
                left_segments: vec![StatusBarSegment {
                    text: format!(" {line}"),
                    fg,
                    bg,
                    bold: false,
                    action_id: None,
                }],
                right_segments: vec![],
            };
            let _ = backend.draw_status_bar_interactive(
                Rect::new(content.x, row_y, content.width, lh),
                &bar,
                &InteractionState::new(),
            );
            rendered_rows = i + 1;
        }
        backend.register_text_region(TextRegion {
            id: WidgetId::new(CONTENT_ID),
            bounds: Rect::new(
                content.x,
                content.y,
                content.width,
                rendered_rows as f32 * lh,
            ),
            lines: lines.iter().map(|s| s.to_string()).collect(),
        });

        let status_bar = StatusBar {
            id: WidgetId::new("gallery:text-selection:status"),
            left_segments: vec![StatusBarSegment {
                text: format!(" {} ", self.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(
            Self::status_rect(area, backend),
            &status_bar,
            &InteractionState::new(),
        );
    }

    fn handle(
        &mut self,
        _variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        match event {
            UiEvent::TextCopied(text) => {
                let preview: String = text.chars().take(40).collect();
                let ellipsis = if text.chars().count() > 40 { "…" } else { "" };
                self.status =
                    format!("Copied: \"{preview}{ellipsis}\" — drag or Ctrl-A to select again");
                Reaction::Redraw
            }
            UiEvent::TextSelectionChanged { anchor, focus, .. } => {
                // Rows are reported relative to the content area's own
                // top edge, not the whole demo area — subtract
                // `content.y` first, or this echoes absolute screen rows
                // (e.g. "rows 6–8" for the demo's first three lines).
                let content = Self::content_rect(area, backend);
                let lh = backend.line_height();
                let a_row = ((anchor.y - content.y) / lh).floor() as usize + 1;
                let f_row = ((focus.y - content.y) / lh).floor() as usize + 1;
                let (start, end) = if a_row <= f_row {
                    (a_row, f_row)
                } else {
                    (f_row, a_row)
                };
                self.status = if start == end {
                    format!("Selecting row {start} — Ctrl-C to copy")
                } else {
                    format!("Selecting rows {start}–{end} — Ctrl-C to copy")
                };
                Reaction::Redraw
            }
            UiEvent::MouseDown { .. } => {
                self.status = hint();
                Reaction::Redraw
            }
            _ => Reaction::Continue,
        }
    }

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

    fn data(&self, variant: usize) -> serde_json::Value {
        serde_json::json!({
            "lines": Self::lines(variant),
            "status": self.status,
        })
    }

    fn caps_note(&self, _variant: usize, caps: &BackendCaps) -> Option<String> {
        if caps.text_selection {
            None
        } else {
            Some(
                "This backend doesn't implement drag-to-select — drag and Ctrl-C/Ctrl-A \
                 have no visible effect here."
                    .to_string(),
            )
        }
    }
}

fn hint() -> String {
    "drag to select · Ctrl-A select all · Ctrl-C copy".into()
}

Backends

VariantTUIGTKmacOSWindows
Pangrams
Haiku