Files
galaxy/crates/editor/src/render/model/positioned.rs
T

247 lines
8.4 KiB
Rust

//! Utilities for traversing laid-out blocks along with their positioning
//! information.
use std::sync::Arc;
use galaxyui::{
geometry::vector::Vector2F,
text_layout::Line,
units::{IntoPixels, Pixels},
};
use sum_tree::{Cursor, Dimension};
use crate::render::layout::line_height;
use string_offset::CharOffset;
use super::{
BlockItem, BlockSpacing, HorizontalRuleConfig, ImageBlockConfig, LaidOutEmbeddedItem,
LaidOutTable, LayoutSummary, LineCount, Paragraph, ParagraphBlock, RenderContext, bounds,
};
/// Wrapper to track an item's position, both in the buffer and on the screen.
#[derive(Debug)]
pub struct Positioned<'a, T> {
/// The starting character offset of this item, relative to the start of
/// the buffer.
pub start_char_offset: CharOffset,
/// The starting line number of this item.
pub start_line: LineCount,
/// The starting y-offset of this item, relative to the origin of the laid-out
/// content.
// TODO: There are at least 4 valid origins for pixel coordinates (content start,
// viewport start, paint origin, and block start). If that starts getting
// confusing, we might want to introduce wrappers similar to `DisplayPoint`
// and `SoftWrapPoint` in the input editor or `WithinBlock` and `WithinModel`
// in the terminal.
pub start_y_offset: Pixels,
pub style: BlockSpacing,
pub item: &'a T,
}
pub trait PositionedCursor<'a> {
/// The block at the current cursor position, along with its position.
fn positioned_item(&self) -> Option<Positioned<'a, BlockItem>>;
}
impl<'a, S: Dimension<'a, LayoutSummary>> PositionedCursor<'a>
for Cursor<'a, BlockItem, S, LayoutSummary>
{
fn positioned_item(&self) -> Option<Positioned<'a, BlockItem>> {
let item = self.item()?;
let summary = self.start();
Some(Positioned {
start_char_offset: summary.content_length,
start_y_offset: (summary.height as f32).into_pixels(),
start_line: summary.lines,
style: item.spacing(),
item,
})
}
}
impl<T> Positioned<'_, T> {
/// The origin of this item's content, relative to the start of the buffer.
pub fn content_origin(&self) -> Vector2F {
bounds::content_origin(self.start_y_offset, &self.style)
}
/// The visible origin of this item, relative to the start of the buffer.
pub fn visible_origin(&self) -> Vector2F {
bounds::visible_origin(self.start_y_offset, &self.style)
}
/// The origin of this item, relative to the start of the buffer, with no padding
/// or margin.
pub fn reserved_origin(&self) -> Vector2F {
bounds::reserved_origin(self.start_y_offset)
}
/// The origin of this item in rendering coordinates.
pub fn render_origin(&self, ctx: &RenderContext) -> Vector2F {
ctx.content_to_screen(self.reserved_origin())
}
}
/// Helpers specific to positioned [`Line`]s.
impl Positioned<'_, Line> {
pub fn end_y_offset(&self) -> Pixels {
self.start_y_offset + line_height(self.item).into_pixels() + self.style.top_offset()
}
}
/// Helpers specific to a positioned [`BlockItem`].
impl<'a> Positioned<'a, BlockItem> {
/// The ending character offset of this item (exclusive).
pub fn end_char_offset(&self) -> CharOffset {
self.start_char_offset + self.item.content_length()
}
/// Check if this block item contains a content offset.
pub fn contains_content(&self, offset: CharOffset) -> bool {
self.start_char_offset <= offset && self.end_char_offset() > offset
}
/// The ending line number of this item (exclusive).
pub fn end_line(&self) -> LineCount {
self.start_line + self.item.lines()
}
/// Position this item's code block.
pub fn code_block(&self, block: &'a ParagraphBlock) -> Positioned<'a, ParagraphBlock> {
debug_assert!(
matches!(self.item, BlockItem::RunnableCodeBlock { .. }),
"Must be a runnable code block"
);
self.position(block)
}
pub fn temporary_block(&self, block: &'a ParagraphBlock) -> Positioned<'a, ParagraphBlock> {
debug_assert!(
matches!(self.item, BlockItem::TemporaryBlock { .. }),
"Must be a temporary block"
);
self.position(block)
}
pub fn embedded(
&self,
embedded_item: &'a Arc<dyn LaidOutEmbeddedItem>,
) -> Positioned<'a, Arc<dyn LaidOutEmbeddedItem>> {
debug_assert!(
matches!(self.item, BlockItem::Embedded(_)),
"Must be an embedded object"
);
self.position(embedded_item)
}
pub fn task_list(&self, paragraph: &'a Paragraph) -> Positioned<'a, Paragraph> {
debug_assert!(
matches!(self.item, BlockItem::TaskList { .. }),
"Must be a task list block"
);
self.position(paragraph)
}
pub fn unordered_list(&self, paragraph: &'a Paragraph) -> Positioned<'a, Paragraph> {
debug_assert!(
matches!(self.item, BlockItem::UnorderedList { .. }),
"Must be an unordered list block"
);
self.position(paragraph)
}
/// Position the content paragraph for an ordered list item.
pub fn ordered_list(&self, paragraph: &'a Paragraph) -> Positioned<'a, Paragraph> {
debug_assert!(
matches!(self.item, BlockItem::OrderedList { .. }),
"Must be an ordered list block"
);
self.position(paragraph)
}
pub fn header(&self, paragraph: &'a Paragraph) -> Positioned<'a, Paragraph> {
debug_assert!(
matches!(self.item, BlockItem::Header { .. }),
"Must be a header block"
);
self.position_centered(paragraph, paragraph.height())
}
/// Position this item's paragraph.
pub fn paragraph(&self, paragraph: &'a Paragraph) -> Positioned<'a, Paragraph> {
debug_assert!(
matches!(self.item, BlockItem::Paragraph(_)),
"Must be a paragraph"
);
// Short paragraphs may have extra padding to meet the minimum paragraph height.
self.position_centered(paragraph, paragraph.height())
}
pub fn text_block(
&self,
paragraph_block: &'a ParagraphBlock,
) -> Positioned<'a, ParagraphBlock> {
debug_assert!(
matches!(self.item, BlockItem::TextBlock { .. }),
"Must be a text block"
);
self.position(paragraph_block)
}
/// Position the trailing newline cursor.
pub fn trailing_newline(&self, cursor: &'a super::Cursor) -> Positioned<'a, super::Cursor> {
// Match the spacing behavior of single-line paragraphs.
self.position_centered(cursor, cursor.height)
}
pub fn horizontal_rule(
&self,
horizontal_rule: &'a HorizontalRuleConfig,
) -> Positioned<'a, HorizontalRuleConfig> {
self.position_centered(horizontal_rule, horizontal_rule.line_height)
}
pub fn image(&self, image_config: &'a ImageBlockConfig) -> Positioned<'a, ImageBlockConfig> {
self.position_centered(image_config, image_config.height)
}
pub fn table(&self, laid_out_table: &'a LaidOutTable) -> Positioned<'a, LaidOutTable> {
debug_assert!(
matches!(self.item, BlockItem::Table(_)),
"Must be a table block"
);
self.position_centered(laid_out_table, laid_out_table.height())
}
/// Helper to position-wrap an item's contents when pattern-matching.
///
/// ```ignore
/// match positioned.item {
/// BlockItem::Paragraph(paragraph) => positioned.position(paragraph),
/// }
/// ```
fn position<T>(&self, content: &'a T) -> Positioned<'a, T> {
Positioned {
start_char_offset: self.start_char_offset,
start_y_offset: self.start_y_offset,
start_line: self.start_line,
style: self.style,
item: content,
}
}
/// Like `positioned`, but centers the content based on the block's expected content height.
/// Use this for blocks with a minimum height.
fn position_centered<T>(
&self,
content: &'a T,
actual_content_height: Pixels,
) -> Positioned<'a, T> {
let mut positioned = self.position(content);
let gap = self.item.content_height() - actual_content_height;
positioned.start_y_offset += gap / 2.0.into_pixels();
positioned
}
}