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,
/// `` alone on a line. Its content is in [`Block::image`].
Image,
}
/// A block-level image: an `` 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
}
}