block.rs raw

//! The document's internal shape: one block per heading, paragraph, code
//! block, list item, quoted paragraph, or thematic break.
//!
//! **This never crosses the `.gdc` boundary.** A `.gdc` writes
//! `(markdown :source page-text)` and the string is the whole interface; that
//! the widget turns it into these is the widget's business, exactly as it is
//! the button's business that its label becomes a parley layout. A document is
//! not a declaration.
//!
//! A block is one [`Paragraph`] (two, for a list item: the marker is laid out
//! separately so it never wraps into the body), because a `Widget` cannot own
//! child widgets — the tree owns children — so the document has to measure and
//! paint itself. That is fine, and it is the reason `Paragraph` is a
//! `guiduck-core` type rather than something inside `Text`.

use guiduck_core::graphic::Graphic;
use guiduck_core::text::Paragraph;
use guiduck_scene::geom::Point;

/// What a block *is*, which is the only thing that decides how it is dressed
/// and placed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlockKind {
    /// `# …` — the level, 1–6.
    Heading(u8),
    Paragraph,
    /// A fenced or indented code block: monospaced, on its own background.
    Code,
    /// One item of a bulleted or numbered list. Carries a marker paragraph.
    ListItem,
    /// `---`. The one block with nothing to say, so it has no paragraph.
    Rule,
    /// A pipe table. Its cells are in [`Block::table`], not the single body.
    Table,
    /// `![alt](src)` alone on a line. Its content is in [`Block::image`].
    Image,
}

/// A block-level image: an `![alt](src)` that is the whole of its paragraph.
///
/// The `src` is opaque to the widget — an application-supplied resolver turns it
/// into pixels, exactly as a link handler turns a target string into a
/// navigation. Until (or unless) it resolves, the alt text stands in, so a
/// document with an unresolvable image still reads.
pub struct ImageContent {
    pub src: String,
    /// The alt text, laid out as the fallback shown when `graphic` is `None`.
    pub alt: Paragraph,
    /// The decoded image the resolver supplied, if any.
    pub graphic: Option<Graphic>,
    /// The resolver reported the image as still loading (`DeferredImage`): the
    /// alt text stands in for now, and the widget keeps asking until it
    /// resolves or is declined.
    pub deferred: bool,
}

/// A column's horizontal alignment, from the table's delimiter row.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CellAlign {
    Start,
    Center,
    End,
}

/// One cell of a table: its prose, and where the prose landed once the grid was
/// placed.
pub struct TableCell {
    pub body: Paragraph,
    /// The cell content's top-left in the widget's coordinates, filled by
    /// layout. Aligned within the column, so it is not simply the column's
    /// left edge.
    pub origin: Point,
}

/// A table: a grid of [`TableCell`]s, the header rows first, plus the geometry
/// the layout pass resolves.
///
/// Rows are padded to the column count, so `rows[r][c]` is always valid for
/// `c < aligns.len()` — a ragged row from a short source line is completed with
/// empty cells rather than special-cased at every use.
pub struct Table {
    /// Per-column alignment; `aligns.len()` is the column count.
    pub aligns: Vec<CellAlign>,
    /// Every row, the first `header_rows` of them the header.
    pub rows: Vec<Vec<TableCell>>,
    /// How many leading rows are the header (1 for a CommonMark table).
    pub header_rows: usize,

    /// Column boundaries in widget coordinates: `col_edges[c]..col_edges[c + 1]`
    /// is column `c`'s box, borders included. Length is the column count + 1.
    pub col_edges: Vec<f64>,
    /// Row boundaries in widget coordinates, the same shape as
    /// [`col_edges`](Self::col_edges) down the other axis.
    pub row_edges: Vec<f64>,
}

impl Table {
    pub fn columns(&self) -> usize {
        self.aligns.len()
    }
}

pub struct Block {
    pub kind: BlockKind,
    /// How many block quotes enclose this block; 0 is unquoted. A count rather
    /// than nesting, because what it buys — one rule per level down the left,
    /// and the content pushed in that far — is a function of the number alone.
    pub quote_depth: u8,
    /// How many lists enclose this block; 0 is not in a list. The item's own
    /// level is included, so an item at depth 1 has its body one
    /// `list-indent` in and its marker at the enclosing left edge.
    pub list_depth: usize,
    /// A list item's `•` or `3.`, laid out on its own so a long body never
    /// wraps underneath it and a wide marker never squeezes it.
    pub marker: Option<Paragraph>,
    /// The block's prose. A [`BlockKind::Rule`] and a [`BlockKind::Table`] have
    /// none — a table's text is in its cells.
    pub body: Option<Paragraph>,
    /// The grid, for a [`BlockKind::Table`].
    pub table: Option<Table>,
    /// The picture, for a [`BlockKind::Image`].
    pub image: Option<ImageContent>,

    /// Where the body's top-left landed in the widget's coordinates. Filled by
    /// the widget's layout pass; meaningless before it.
    pub body_origin: Point,
    /// Where the marker's top-left landed.
    pub marker_origin: Point,
    /// The block's own vertical extent — the padded box for a code block, the
    /// rule's thickness for a break — which is what the quote bars and the code
    /// background are drawn against and what hit testing narrows on.
    pub top: f64,
    pub height: f64,
    /// The block's left edge: quote and list nesting, resolved to pixels.
    pub indent: f64,
}

impl Block {
    pub fn new(
        kind: BlockKind,
        quote_depth: u8,
        list_depth: usize,
        body: Option<Paragraph>,
    ) -> Self {
        Self {
            kind,
            quote_depth,
            list_depth,
            marker: None,
            body,
            table: None,
            image: None,
            body_origin: Point::ORIGIN,
            marker_origin: Point::ORIGIN,
            top: 0.0,
            height: 0.0,
            indent: 0.0,
        }
    }

    /// Whether `y`, in the widget's coordinates, is within this block.
    pub fn contains_y(&self, y: f64) -> bool {
        y >= self.top && y < self.top + self.height
    }
}