7125 lines
232 KiB
Rust
7125 lines
232 KiB
Rust
use etaf_core::{diff_commit_batch, CommitBatch, SpanEdit};
|
|
use serde::Deserialize;
|
|
use std::collections::BTreeMap;
|
|
|
|
#[cfg(test)]
|
|
use std::cell::Cell;
|
|
|
|
#[cfg(test)]
|
|
thread_local! {
|
|
static TEST_RENDER_NODE_COUNT: Cell<Option<usize>> = const { Cell::new(None) };
|
|
static TEST_DISABLE_WINDOW_RENDER: Cell<bool> = const { Cell::new(false) };
|
|
}
|
|
|
|
const LAYOUT_VERSION: u32 = 2;
|
|
pub const TAPE_VERSION: u16 = 12;
|
|
pub const TAPE_HEADER_LEN: usize = 112;
|
|
pub const MIN_TAPE_BYTES: usize = TAPE_HEADER_LEN + 5;
|
|
const TAPE_MAGIC: &[u8; 4] = b"EBXT";
|
|
const TAPE_FLAG_OK: u16 = 1;
|
|
const TAPE_FLAG_COMPLETE: u16 = 1 << 1;
|
|
const TAPE_FLAG_PATCH: u16 = 1 << 2;
|
|
const MAX_LAYOUT_DEPTH: usize = 256;
|
|
const MAX_LAYOUT_NODES: usize = 100_000;
|
|
pub const MAX_LAYOUT_DIMENSION: i64 = 1_000_000;
|
|
const MAX_LAYOUT_WORK_UNITS: usize = 250_000;
|
|
const MAX_TAPE_PROPERTY_ENTRIES: usize = MAX_LAYOUT_DEPTH * 16;
|
|
const MAX_PROPERTY_TEMPLATE_COUNT: u32 = MAX_LAYOUT_NODES as u32;
|
|
const MAX_TAPE_METADATA_RECORDS: usize = MAX_LAYOUT_WORK_UNITS;
|
|
const MAX_STYLE_STRING_BYTES: usize = 4096;
|
|
|
|
const METADATA_ROLE_CONTENT: u8 = 1;
|
|
const METADATA_ROLE_CONTENT_OWNER: u8 = 2;
|
|
const METADATA_ROLE_PADDING_TOP: u8 = 3;
|
|
const METADATA_ROLE_PADDING_BOTTOM: u8 = 4;
|
|
const METADATA_ROLE_PADDING_LEFT: u8 = 5;
|
|
const METADATA_ROLE_PADDING_RIGHT: u8 = 6;
|
|
const METADATA_ROLE_MARGIN_TOP: u8 = 7;
|
|
const METADATA_ROLE_MARGIN_BOTTOM: u8 = 8;
|
|
const METADATA_ROLE_MARGIN_LEFT: u8 = 9;
|
|
const METADATA_ROLE_MARGIN_RIGHT: u8 = 10;
|
|
const METADATA_ROLE_BORDER_TOP: u8 = 11;
|
|
const METADATA_ROLE_BORDER_BOTTOM: u8 = 12;
|
|
const METADATA_ROLE_BORDER_LEFT: u8 = 13;
|
|
const METADATA_ROLE_BORDER_RIGHT: u8 = 14;
|
|
const METADATA_BOX_EXTENT: u8 = 15;
|
|
const METADATA_SCROLL_CONTENT: u8 = 16;
|
|
const METADATA_SCROLL_OWNER: u8 = 17;
|
|
const METADATA_SCROLL_WINDOW: u8 = 18;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
|
pub struct LayoutDocument {
|
|
version: u32,
|
|
space_width: i64,
|
|
style_count: u32,
|
|
#[serde(default)]
|
|
property_template_count: u32,
|
|
#[serde(default)]
|
|
pub(crate) styles: Vec<StyleTemplate>,
|
|
root: LayoutNode,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
|
pub(crate) struct StyleTemplate {
|
|
mode: StyleMode,
|
|
face: FaceTemplate,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum StyleMode {
|
|
Set,
|
|
Add,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
|
struct FaceTemplate {
|
|
#[serde(default)]
|
|
lisp: Option<String>,
|
|
#[serde(default)]
|
|
inherit: Option<String>,
|
|
#[serde(default)]
|
|
inverse_video: Option<bool>,
|
|
#[serde(default)]
|
|
foreground: Option<String>,
|
|
#[serde(default)]
|
|
background: Option<String>,
|
|
#[serde(default)]
|
|
overline: Option<ColorOrTrue>,
|
|
#[serde(default)]
|
|
underline: Option<UnderlineTemplate>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
#[serde(untagged)]
|
|
enum ColorOrTrue {
|
|
Boolean(bool),
|
|
Color(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
|
struct UnderlineTemplate {
|
|
position: bool,
|
|
#[serde(default)]
|
|
color: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
// Keeping the node payload inline avoids another allocation on the hot layout path.
|
|
#[allow(clippy::large_enum_variant)]
|
|
#[serde(
|
|
tag = "type",
|
|
rename_all = "kebab-case",
|
|
rename_all_fields = "kebab-case",
|
|
deny_unknown_fields
|
|
)]
|
|
enum LayoutNode {
|
|
Box {
|
|
region_id: i64,
|
|
content: Option<Box<MeasuredText>>,
|
|
child: Option<Box<LayoutNode>>,
|
|
content_width_exact: bool,
|
|
#[serde(default)]
|
|
content_min_width: Option<i64>,
|
|
width: Size,
|
|
min_width: Size,
|
|
max_width: Size,
|
|
height: Size,
|
|
min_height: Size,
|
|
max_height: Size,
|
|
box_sizing: BoxSizing,
|
|
padding_left: i64,
|
|
padding_right: i64,
|
|
padding_top: i64,
|
|
padding_bottom: i64,
|
|
margin_left: i64,
|
|
margin_right: i64,
|
|
margin_top: i64,
|
|
margin_bottom: i64,
|
|
border_left: i64,
|
|
border_right: i64,
|
|
foreground_style: Option<u32>,
|
|
background_style: Option<u32>,
|
|
border_left_style: Option<u32>,
|
|
border_right_style: Option<u32>,
|
|
border_top_style: Option<u32>,
|
|
border_bottom_style: Option<u32>,
|
|
#[serde(default)]
|
|
surface_template_id: Option<u32>,
|
|
text_align: HorizontalAlign,
|
|
vertical_align: VerticalAlign,
|
|
overflow: Overflow,
|
|
wrap_mode: WrapMode,
|
|
scroll_offset: i64,
|
|
},
|
|
Row {
|
|
children: Vec<LayoutNode>,
|
|
},
|
|
Column {
|
|
children: Vec<LayoutNode>,
|
|
},
|
|
Flex {
|
|
direction: FlexDirection,
|
|
wrap: FlexWrap,
|
|
justify: FlexAlign,
|
|
align_items: FlexAlign,
|
|
align_content: FlexAlign,
|
|
width: Size,
|
|
height: Size,
|
|
row_gap: i64,
|
|
column_gap: i64,
|
|
items: Vec<FlexItem>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
|
|
enum Size {
|
|
Auto,
|
|
Content,
|
|
None,
|
|
Pixels {
|
|
value: i64,
|
|
},
|
|
Lines {
|
|
value: i64,
|
|
},
|
|
Viewport,
|
|
ViewportHeight,
|
|
MinContent,
|
|
MaxContent,
|
|
FitContent {
|
|
#[serde(default)]
|
|
limit: Option<Box<Size>>,
|
|
},
|
|
Stretch,
|
|
Contain,
|
|
Add {
|
|
values: Box<SizeValues>,
|
|
},
|
|
Subtract {
|
|
values: Box<SizeValues>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(transparent)]
|
|
struct SizeValues(Box<[Size]>);
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum FlexDirection {
|
|
Row,
|
|
RowReverse,
|
|
Column,
|
|
ColumnReverse,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum FlexWrap {
|
|
Nowrap,
|
|
Wrap,
|
|
WrapReverse,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum FlexAlign {
|
|
Auto,
|
|
Normal,
|
|
Stretch,
|
|
FlexStart,
|
|
FlexEnd,
|
|
Center,
|
|
Start,
|
|
End,
|
|
SelfStart,
|
|
SelfEnd,
|
|
Left,
|
|
Right,
|
|
Top,
|
|
Bottom,
|
|
Baseline,
|
|
SpaceBetween,
|
|
SpaceAround,
|
|
SpaceEvenly,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
|
struct FlexItem {
|
|
node: LayoutNode,
|
|
order: i64,
|
|
grow: f64,
|
|
shrink: f64,
|
|
basis: Size,
|
|
align_self: FlexAlign,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum BoxSizing {
|
|
ContentBox,
|
|
BorderBox,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum HorizontalAlign {
|
|
Left,
|
|
Center,
|
|
Right,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum VerticalAlign {
|
|
Top,
|
|
Center,
|
|
Bottom,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum Overflow {
|
|
Scroll,
|
|
Hidden,
|
|
Visible,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum WrapMode {
|
|
None,
|
|
Word,
|
|
Char,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct MeasuredText {
|
|
lines: Vec<MeasuredLine>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct MeasuredLine {
|
|
clusters: Vec<MeasuredCluster>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct MeasuredCluster {
|
|
text: String,
|
|
width: i64,
|
|
cjk: bool,
|
|
space: bool,
|
|
#[serde(rename = "pixel-space", default)]
|
|
pixel_space: bool,
|
|
#[serde(default, rename = "source-template-id")]
|
|
source_template_id: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct LayoutContext {
|
|
pub viewport_width: i64,
|
|
pub viewport_width_known: bool,
|
|
pub viewport_height: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct TapeIdentity {
|
|
pub session_id: u64,
|
|
pub generation: u64,
|
|
pub key: i64,
|
|
pub runtime_revision: u64,
|
|
pub context_hash: i64,
|
|
pub viewport_width: i64,
|
|
pub viewport_height: i64,
|
|
pub root_width: i64,
|
|
pub complete: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct TapeOutputOptions {
|
|
pub root_metadata: bool,
|
|
pub max_bytes: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct LayoutTape {
|
|
style_count: u32,
|
|
lines: Vec<TapeLine>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct TapeCharacter {
|
|
value: char,
|
|
pixel_width: Option<u64>,
|
|
properties: AtomProperties,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct FlatLayoutTape {
|
|
style_count: u32,
|
|
line_count: u32,
|
|
characters: Vec<TapeCharacter>,
|
|
}
|
|
|
|
type TapePatch = SpanEdit;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct TapeLine {
|
|
width: i64,
|
|
atoms: Vec<TapeAtom>,
|
|
break_after: Option<AtomProperties>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
enum TapeAtom {
|
|
Text {
|
|
text: String,
|
|
width: i64,
|
|
properties: AtomProperties,
|
|
},
|
|
Space {
|
|
width: i64,
|
|
properties: AtomProperties,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
struct AtomProperties {
|
|
style_ids: Vec<u32>,
|
|
content: Option<i64>,
|
|
content_idx: Option<i64>,
|
|
owner: Option<i64>,
|
|
owners: Vec<i64>,
|
|
roles: Vec<RegionRoleEntry>,
|
|
scroll_window: Option<i64>,
|
|
property_template_ids: Vec<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RegionRole {
|
|
PaddingLeft,
|
|
PaddingRight,
|
|
PaddingTop,
|
|
PaddingBottom,
|
|
BorderLeft,
|
|
BorderRight,
|
|
BorderTop,
|
|
BorderBottom,
|
|
MarginLeft,
|
|
MarginRight,
|
|
MarginTop,
|
|
MarginBottom,
|
|
}
|
|
|
|
const REGION_ROLES: [RegionRole; 12] = [
|
|
RegionRole::PaddingLeft,
|
|
RegionRole::PaddingRight,
|
|
RegionRole::PaddingTop,
|
|
RegionRole::PaddingBottom,
|
|
RegionRole::BorderLeft,
|
|
RegionRole::BorderRight,
|
|
RegionRole::BorderTop,
|
|
RegionRole::BorderBottom,
|
|
RegionRole::MarginLeft,
|
|
RegionRole::MarginRight,
|
|
RegionRole::MarginTop,
|
|
RegionRole::MarginBottom,
|
|
];
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct RegionRoleEntry {
|
|
role: RegionRole,
|
|
region_id: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum Atom {
|
|
Text {
|
|
text: String,
|
|
width: i64,
|
|
cjk: bool,
|
|
space: bool,
|
|
properties: AtomProperties,
|
|
},
|
|
Space {
|
|
width: i64,
|
|
properties: AtomProperties,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
struct Line {
|
|
atoms: Vec<Atom>,
|
|
width: i64,
|
|
}
|
|
|
|
impl Line {
|
|
fn from_clusters(clusters: &[MeasuredCluster]) -> Self {
|
|
let mut line = Self::default();
|
|
for cluster in clusters {
|
|
let properties = AtomProperties {
|
|
property_template_ids: cluster.source_template_id.into_iter().collect(),
|
|
..AtomProperties::default()
|
|
};
|
|
if cluster.pixel_space {
|
|
line.push_space_with_properties(cluster.width, properties);
|
|
} else {
|
|
line.width += cluster.width;
|
|
line.atoms.push(Atom::Text {
|
|
text: cluster.text.clone(),
|
|
width: cluster.width,
|
|
cjk: cluster.cjk,
|
|
space: cluster.space,
|
|
properties,
|
|
});
|
|
}
|
|
}
|
|
line
|
|
}
|
|
|
|
fn from_atoms(atoms: &[Atom]) -> Self {
|
|
Self {
|
|
atoms: atoms.to_vec(),
|
|
width: atoms.iter().map(Atom::width).sum(),
|
|
}
|
|
}
|
|
|
|
fn push_space(&mut self, width: i64) {
|
|
self.push_space_with_properties(width, AtomProperties::default());
|
|
}
|
|
|
|
fn push_space_with_properties(&mut self, width: i64, properties: AtomProperties) {
|
|
if width <= 0 {
|
|
return;
|
|
}
|
|
self.width += width;
|
|
self.atoms.push(Atom::Space { width, properties });
|
|
}
|
|
|
|
fn prepend_space(&mut self, width: i64) {
|
|
self.prepend_space_with_properties(width, AtomProperties::default());
|
|
}
|
|
|
|
fn prepend_space_with_properties(&mut self, width: i64, properties: AtomProperties) {
|
|
if width <= 0 {
|
|
return;
|
|
}
|
|
self.width += width;
|
|
self.atoms.insert(0, Atom::Space { width, properties });
|
|
}
|
|
|
|
fn append(&mut self, other: &Self) {
|
|
self.width += other.width;
|
|
self.atoms.extend(other.atoms.iter().cloned());
|
|
}
|
|
|
|
fn padded(mut self, target: i64, align: HorizontalAlign) -> Self {
|
|
let remaining = (target - self.width).max(0);
|
|
let left = match align {
|
|
HorizontalAlign::Left => 0,
|
|
HorizontalAlign::Right => remaining,
|
|
HorizontalAlign::Center => remaining / 2,
|
|
};
|
|
self.prepend_space(left);
|
|
self.push_space(remaining - left);
|
|
self
|
|
}
|
|
|
|
fn blank(width: i64) -> Self {
|
|
let mut line = Self::default();
|
|
line.push_space(width);
|
|
line
|
|
}
|
|
|
|
fn blank_with_properties(width: i64, properties: AtomProperties) -> Self {
|
|
let mut line = Self::default();
|
|
line.push_space_with_properties(width, properties);
|
|
line
|
|
}
|
|
|
|
fn whitespace_only(&self) -> bool {
|
|
self.atoms.iter().all(|atom| match atom {
|
|
Atom::Space { .. } => true,
|
|
Atom::Text { text, .. } => text.trim().is_empty(),
|
|
})
|
|
}
|
|
|
|
fn has_noncontent_properties(&self) -> bool {
|
|
self.atoms.iter().any(|atom| {
|
|
let properties = atom.properties();
|
|
!properties.style_ids.is_empty()
|
|
|| !properties.roles.is_empty()
|
|
|| !properties.property_template_ids.is_empty()
|
|
})
|
|
}
|
|
|
|
fn own_content(&mut self, region_id: i64, content_idx: i64) {
|
|
let has_owner = self
|
|
.atoms
|
|
.iter()
|
|
.any(|atom| atom.properties().owner.is_some() || !atom.properties().owners.is_empty());
|
|
for atom in &mut self.atoms {
|
|
let properties = atom.properties_mut();
|
|
if has_owner {
|
|
let mut owners = properties.owners.clone();
|
|
if let Some(owner) = properties.owner {
|
|
if !owners.contains(&owner) {
|
|
owners.push(owner);
|
|
}
|
|
}
|
|
if !owners.contains(®ion_id) {
|
|
owners.push(region_id);
|
|
}
|
|
properties.owner = Some(region_id);
|
|
properties.owners = owners;
|
|
} else {
|
|
properties.owner = Some(region_id);
|
|
properties.owners = vec![region_id];
|
|
}
|
|
}
|
|
let has_content = self
|
|
.atoms
|
|
.iter()
|
|
.any(|atom| atom.properties().content.is_some());
|
|
if !has_content {
|
|
for atom in &mut self.atoms {
|
|
let properties = atom.properties_mut();
|
|
properties.content = Some(region_id);
|
|
properties.content_idx = Some(content_idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn collapse_whitespace_content(self, width: i64, region_id: i64) -> Self {
|
|
if !self.whitespace_only() || self.has_noncontent_properties() {
|
|
return self;
|
|
}
|
|
let first = self.atoms.first().map(Atom::properties);
|
|
let mut properties = AtomProperties {
|
|
content: first
|
|
.and_then(|properties| properties.content)
|
|
.or(Some(region_id)),
|
|
content_idx: first
|
|
.and_then(|properties| properties.content_idx)
|
|
.or(Some(0)),
|
|
owner: Some(region_id),
|
|
..AtomProperties::default()
|
|
};
|
|
if properties.content.is_none() {
|
|
properties.content = Some(region_id);
|
|
}
|
|
let mut line = Self::default();
|
|
line.push_space_with_properties(width, properties);
|
|
line
|
|
}
|
|
|
|
fn apply_style(&mut self, style_id: Option<u32>) {
|
|
if let Some(style_id) = style_id {
|
|
for atom in &mut self.atoms {
|
|
atom.properties_mut().style_ids.push(style_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_property_template(&mut self, template_id: Option<u32>) {
|
|
if let Some(template_id) = template_id {
|
|
for atom in &mut self.atoms {
|
|
atom.properties_mut()
|
|
.property_template_ids
|
|
.push(template_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_role(&mut self, role: RegionRole, region_id: i64) {
|
|
for atom in &mut self.atoms {
|
|
atom.properties_mut()
|
|
.roles
|
|
.push(RegionRoleEntry { role, region_id });
|
|
}
|
|
}
|
|
}
|
|
|
|
fn region_properties(role: RegionRole, region_id: i64, style_id: Option<u32>) -> AtomProperties {
|
|
AtomProperties {
|
|
style_ids: style_id.into_iter().collect(),
|
|
roles: vec![RegionRoleEntry { role, region_id }],
|
|
..AtomProperties::default()
|
|
}
|
|
}
|
|
|
|
impl Atom {
|
|
fn width(&self) -> i64 {
|
|
match self {
|
|
Self::Text { width, .. } | Self::Space { width, .. } => *width,
|
|
}
|
|
}
|
|
|
|
fn wrap_space(&self) -> bool {
|
|
match self {
|
|
Self::Text { space, .. } => *space,
|
|
Self::Space { .. } => true,
|
|
}
|
|
}
|
|
|
|
fn wrap_cjk(&self) -> bool {
|
|
matches!(self, Self::Text { cjk: true, .. })
|
|
}
|
|
|
|
fn properties(&self) -> &AtomProperties {
|
|
match self {
|
|
Self::Text { properties, .. } | Self::Space { properties, .. } => properties,
|
|
}
|
|
}
|
|
|
|
fn properties_mut(&mut self) -> &mut AtomProperties {
|
|
match self {
|
|
Self::Text { properties, .. } | Self::Space { properties, .. } => properties,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Rendered {
|
|
lines: Vec<Line>,
|
|
breaks: Vec<AtomProperties>,
|
|
}
|
|
|
|
impl Rendered {
|
|
fn from_lines(lines: Vec<Line>) -> Self {
|
|
let breaks = vec![AtomProperties::default(); lines.len().saturating_sub(1)];
|
|
Self { lines, breaks }
|
|
}
|
|
|
|
fn first_width(&self) -> i64 {
|
|
self.lines.first().map_or(0, |line| line.width)
|
|
}
|
|
|
|
fn max_width(&self) -> i64 {
|
|
self.lines.iter().map(|line| line.width).max().unwrap_or(0)
|
|
}
|
|
|
|
fn min_content_width(&self, wrap_mode: WrapMode) -> i64 {
|
|
if wrap_mode == WrapMode::None {
|
|
return self.max_width();
|
|
}
|
|
self.lines
|
|
.iter()
|
|
.map(|line| {
|
|
let mut maximum = 0;
|
|
let mut run = 0;
|
|
for atom in &line.atoms {
|
|
if atom.wrap_space() {
|
|
maximum = maximum.max(run);
|
|
run = 0;
|
|
} else if atom.wrap_cjk() {
|
|
maximum = maximum.max(run).max(atom.width());
|
|
run = 0;
|
|
} else {
|
|
run += atom.width();
|
|
}
|
|
}
|
|
maximum.max(run)
|
|
})
|
|
.max()
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn height(&self) -> i64 {
|
|
self.lines.len() as i64
|
|
}
|
|
|
|
fn apply_scroll_window(&mut self, region_id: i64) {
|
|
for line in &mut self.lines {
|
|
for atom in &mut line.atoms {
|
|
atom.properties_mut().scroll_window = Some(region_id);
|
|
}
|
|
}
|
|
for properties in &mut self.breaks {
|
|
properties.scroll_window = Some(region_id);
|
|
}
|
|
}
|
|
|
|
fn into_tape(self, style_count: u32) -> LayoutTape {
|
|
let last_line = self.lines.len().saturating_sub(1);
|
|
let mut breaks = self.breaks.into_iter();
|
|
LayoutTape {
|
|
style_count,
|
|
lines: self
|
|
.lines
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(index, line)| TapeLine {
|
|
width: line.width,
|
|
atoms: line
|
|
.atoms
|
|
.into_iter()
|
|
.map(|atom| match atom {
|
|
Atom::Text {
|
|
text,
|
|
width,
|
|
properties,
|
|
..
|
|
} => TapeAtom::Text {
|
|
text,
|
|
width,
|
|
properties,
|
|
},
|
|
Atom::Space { width, properties } => {
|
|
TapeAtom::Space { width, properties }
|
|
}
|
|
})
|
|
.collect(),
|
|
break_after: (index < last_line)
|
|
.then(|| breaks.next().expect("rendered break invariant")),
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TapeWriter {
|
|
bytes: Vec<u8>,
|
|
limit: usize,
|
|
}
|
|
|
|
impl TapeWriter {
|
|
fn new(limit: usize) -> Result<Self, String> {
|
|
if limit < TAPE_HEADER_LEN {
|
|
return Err("Native layout tape limit is smaller than its header".to_owned());
|
|
}
|
|
Ok(Self {
|
|
bytes: vec![0; TAPE_HEADER_LEN],
|
|
limit,
|
|
})
|
|
}
|
|
|
|
fn reserve(&self, additional: usize) -> Result<(), String> {
|
|
if self
|
|
.bytes
|
|
.len()
|
|
.checked_add(additional)
|
|
.is_none_or(|length| length > self.limit)
|
|
{
|
|
Err("Native layout tape exceeds the result byte limit".to_owned())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn push_bytes(&mut self, bytes: &[u8]) -> Result<(), String> {
|
|
self.reserve(bytes.len())?;
|
|
self.bytes.extend_from_slice(bytes);
|
|
Ok(())
|
|
}
|
|
|
|
fn push_u32(&mut self, value: u32) -> Result<(), String> {
|
|
self.push_bytes(&value.to_le_bytes())
|
|
}
|
|
|
|
fn push_u64(&mut self, value: u64) -> Result<(), String> {
|
|
self.push_bytes(&value.to_le_bytes())
|
|
}
|
|
|
|
fn patch_u16(&mut self, offset: usize, value: u16) {
|
|
self.bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
|
|
}
|
|
|
|
fn patch_u32(&mut self, offset: usize, value: u32) {
|
|
self.bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
|
}
|
|
|
|
fn patch_u64(&mut self, offset: usize, value: u64) {
|
|
self.bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
|
|
}
|
|
|
|
fn patch_i64(&mut self, offset: usize, value: i64) {
|
|
self.bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
|
|
}
|
|
|
|
fn finish_header(
|
|
mut self,
|
|
identity: TapeIdentity,
|
|
ok: bool,
|
|
patch: bool,
|
|
style_count: u32,
|
|
line_count: u32,
|
|
character_count: u64,
|
|
) -> Vec<u8> {
|
|
let mut flags = if ok { TAPE_FLAG_OK } else { 0 };
|
|
if identity.complete {
|
|
flags |= TAPE_FLAG_COMPLETE;
|
|
}
|
|
if patch {
|
|
flags |= TAPE_FLAG_PATCH;
|
|
}
|
|
let total_len = self.bytes.len() as u64;
|
|
let body_len = total_len - TAPE_HEADER_LEN as u64;
|
|
self.bytes[..4].copy_from_slice(TAPE_MAGIC);
|
|
self.patch_u16(4, TAPE_VERSION);
|
|
self.patch_u16(6, flags);
|
|
self.patch_u32(8, TAPE_HEADER_LEN as u32);
|
|
self.patch_u64(12, total_len);
|
|
self.patch_u64(20, identity.session_id);
|
|
self.patch_u64(28, identity.generation);
|
|
self.patch_i64(36, identity.key);
|
|
self.patch_u64(44, identity.runtime_revision);
|
|
self.patch_i64(52, identity.context_hash);
|
|
self.patch_i64(60, identity.viewport_width);
|
|
self.patch_i64(68, identity.viewport_height);
|
|
self.patch_i64(76, identity.root_width);
|
|
self.patch_u32(84, style_count);
|
|
self.patch_u32(88, line_count);
|
|
self.patch_u64(92, character_count);
|
|
self.patch_u64(100, body_len);
|
|
self.patch_u32(108, 0);
|
|
self.bytes
|
|
}
|
|
}
|
|
|
|
impl RegionRole {
|
|
fn lisp_property(self) -> &'static str {
|
|
match self {
|
|
Self::PaddingLeft => "ebox-pl",
|
|
Self::PaddingRight => "ebox-pr",
|
|
Self::PaddingTop => "ebox-pt",
|
|
Self::PaddingBottom => "ebox-pb",
|
|
Self::BorderLeft => "ebox-bl",
|
|
Self::BorderRight => "ebox-br",
|
|
Self::BorderTop => "ebox-bt",
|
|
Self::BorderBottom => "ebox-bb",
|
|
Self::MarginLeft => "ebox-ml",
|
|
Self::MarginRight => "ebox-mr",
|
|
Self::MarginTop => "ebox-mt",
|
|
Self::MarginBottom => "ebox-mb",
|
|
}
|
|
}
|
|
|
|
fn metadata_kind(self) -> u8 {
|
|
match self {
|
|
Self::PaddingLeft => METADATA_ROLE_PADDING_LEFT,
|
|
Self::PaddingRight => METADATA_ROLE_PADDING_RIGHT,
|
|
Self::PaddingTop => METADATA_ROLE_PADDING_TOP,
|
|
Self::PaddingBottom => METADATA_ROLE_PADDING_BOTTOM,
|
|
Self::BorderLeft => METADATA_ROLE_BORDER_LEFT,
|
|
Self::BorderRight => METADATA_ROLE_BORDER_RIGHT,
|
|
Self::BorderTop => METADATA_ROLE_BORDER_TOP,
|
|
Self::BorderBottom => METADATA_ROLE_BORDER_BOTTOM,
|
|
Self::MarginLeft => METADATA_ROLE_MARGIN_LEFT,
|
|
Self::MarginRight => METADATA_ROLE_MARGIN_RIGHT,
|
|
Self::MarginTop => METADATA_ROLE_MARGIN_TOP,
|
|
Self::MarginBottom => METADATA_ROLE_MARGIN_BOTTOM,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn count_u32(value: usize, label: &str) -> Result<u32, String> {
|
|
u32::try_from(value).map_err(|_| format!("Native layout tape has too many {label}"))
|
|
}
|
|
|
|
fn tape_width(value: i64, label: &str) -> Result<u64, String> {
|
|
u64::try_from(value).map_err(|_| format!("Native layout tape has negative {label}"))
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct TapeSpaceSpan {
|
|
start: u64,
|
|
width: u64,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct TapePropertySpan {
|
|
start: u64,
|
|
end: u64,
|
|
properties: AtomProperties,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct LiteralInterval {
|
|
start: u64,
|
|
end: u64,
|
|
properties: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct TapeMetadataRecord {
|
|
kind: u8,
|
|
region_id: i64,
|
|
index: u32,
|
|
start: u64,
|
|
end: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct FragmentTemplate {
|
|
start: u64,
|
|
end: u64,
|
|
line: u64,
|
|
roles: Vec<(&'static str, i64)>,
|
|
content_owner: Option<i64>,
|
|
content_index: Option<i64>,
|
|
property_template_ids: Vec<u32>,
|
|
style_ids: Vec<u32>,
|
|
}
|
|
|
|
type FragmentRoles = Vec<(&'static str, i64)>;
|
|
type FragmentOwnershipSpan = (u64, u64, FragmentRoles);
|
|
type RegionMountKey = (i64, Vec<&'static str>);
|
|
type RegionMountSpan = (RegionMountKey, u64, u64);
|
|
type FragmentStyleDelta = Vec<(usize, Vec<u32>)>;
|
|
|
|
fn propagated_fragment_roles(fragments: &[FragmentTemplate]) -> Vec<Vec<(&'static str, i64)>> {
|
|
let mut propagated = vec![Vec::new(); fragments.len()];
|
|
let mut previous = Vec::new();
|
|
for (index, fragment) in fragments.iter().enumerate() {
|
|
if fragment.roles.is_empty() {
|
|
propagated[index] = previous.clone();
|
|
} else {
|
|
previous = fragment.roles.clone();
|
|
propagated[index] = previous.clone();
|
|
}
|
|
}
|
|
let mut next = Vec::new();
|
|
for index in (0..fragments.len()).rev() {
|
|
if fragments[index].roles.is_empty() {
|
|
for role in &next {
|
|
if !propagated[index].contains(role) {
|
|
propagated[index].push(*role);
|
|
}
|
|
}
|
|
} else {
|
|
next = fragments[index].roles.clone();
|
|
}
|
|
}
|
|
propagated
|
|
}
|
|
|
|
fn fragment_ownership_spans(fragments: &[FragmentTemplate]) -> Vec<FragmentOwnershipSpan> {
|
|
let mut spans: Vec<FragmentOwnershipSpan> = Vec::new();
|
|
for (fragment, roles) in fragments.iter().zip(propagated_fragment_roles(fragments)) {
|
|
if let Some(previous) = spans.last_mut() {
|
|
if previous.1 == fragment.start && previous.2 == roles {
|
|
previous.1 = fragment.end;
|
|
continue;
|
|
}
|
|
}
|
|
spans.push((fragment.start, fragment.end, roles));
|
|
}
|
|
spans
|
|
}
|
|
|
|
fn fragment_region_mount_projection(fragments: &[FragmentTemplate]) -> Vec<RegionMountSpan> {
|
|
let mut spans: Vec<RegionMountSpan> = Vec::new();
|
|
let mut active: BTreeMap<RegionMountKey, usize> = BTreeMap::new();
|
|
for fragment in fragments {
|
|
let mut roles_by_region: BTreeMap<i64, Vec<&'static str>> = BTreeMap::new();
|
|
for (role, region_id) in &fragment.roles {
|
|
roles_by_region.entry(*region_id).or_default().push(*role);
|
|
}
|
|
for (region_id, roles) in roles_by_region {
|
|
let key = (region_id, roles);
|
|
if let Some(index) = active.get(&key).copied() {
|
|
if spans[index].2 == fragment.start {
|
|
spans[index].2 = fragment.end;
|
|
continue;
|
|
}
|
|
}
|
|
let index = spans.len();
|
|
spans.push((key.clone(), fragment.start, fragment.end));
|
|
active.insert(key, index);
|
|
}
|
|
}
|
|
spans
|
|
}
|
|
|
|
fn fragment_style_delta(
|
|
old: &[FragmentTemplate],
|
|
target: &[FragmentTemplate],
|
|
) -> Option<FragmentStyleDelta> {
|
|
if old.len() != target.len() {
|
|
return None;
|
|
}
|
|
let mut delta = Vec::new();
|
|
for (index, (old, target)) in old.iter().zip(target).enumerate() {
|
|
if old.start != target.start
|
|
|| old.end != target.end
|
|
|| old.line != target.line
|
|
|| old.roles != target.roles
|
|
|| old.content_owner != target.content_owner
|
|
|| old.content_index != target.content_index
|
|
|| old.property_template_ids != target.property_template_ids
|
|
{
|
|
return None;
|
|
}
|
|
if old.style_ids != target.style_ids {
|
|
delta.push((index, target.style_ids.clone()));
|
|
}
|
|
}
|
|
Some(delta)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct CompiledStyle {
|
|
mode: StyleMode,
|
|
face: String,
|
|
}
|
|
|
|
fn validate_style_string(value: &str, label: &str) -> Result<(), String> {
|
|
if value.len() > MAX_STYLE_STRING_BYTES {
|
|
return Err(format!("Native layout {label} exceeds its byte limit"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn push_lisp_string(output: &mut String, value: &str) {
|
|
output.push('"');
|
|
for character in value.chars() {
|
|
match character {
|
|
'"' => output.push_str("\\\""),
|
|
'\\' => output.push_str("\\\\"),
|
|
'\n' => output.push_str("\\n"),
|
|
'\r' => output.push_str("\\r"),
|
|
'\t' => output.push_str("\\t"),
|
|
character if character.is_control() => {
|
|
output.push_str(&format!("\\u{:04x}", character as u32));
|
|
}
|
|
character => output.push(character),
|
|
}
|
|
}
|
|
output.push('"');
|
|
}
|
|
|
|
fn push_property(output: &mut String, first: &mut bool, name: &str, value: &str) {
|
|
if !*first {
|
|
output.push(' ');
|
|
}
|
|
*first = false;
|
|
output.push_str(name);
|
|
output.push(' ');
|
|
output.push_str(value);
|
|
}
|
|
|
|
impl FaceTemplate {
|
|
fn validate(&self) -> Result<(), String> {
|
|
if let Some(lisp) = &self.lisp {
|
|
validate_style_string(lisp, "face literal")?;
|
|
if self.inherit.is_some()
|
|
|| self.inverse_video.is_some()
|
|
|| self.foreground.is_some()
|
|
|| self.background.is_some()
|
|
|| self.overline.is_some()
|
|
|| self.underline.is_some()
|
|
{
|
|
return Err("Native layout literal face cannot mix typed fields".to_owned());
|
|
}
|
|
return Ok(());
|
|
}
|
|
if self.inherit.is_none()
|
|
&& self.inverse_video.is_none()
|
|
&& self.foreground.is_none()
|
|
&& self.background.is_none()
|
|
&& self.overline.is_none()
|
|
&& self.underline.is_none()
|
|
{
|
|
return Err("Native layout face template is empty".to_owned());
|
|
}
|
|
if let Some(inherit) = &self.inherit {
|
|
if inherit != "default" {
|
|
return Err("Native layout inherit face must be default".to_owned());
|
|
}
|
|
}
|
|
if self.inverse_video == Some(false) {
|
|
return Err("Native layout inverse-video face must be true".to_owned());
|
|
}
|
|
if let Some(value) = &self.foreground {
|
|
validate_style_string(value, "foreground color")?;
|
|
}
|
|
if let Some(value) = &self.background {
|
|
validate_style_string(value, "background color")?;
|
|
}
|
|
if let Some(value) = &self.overline {
|
|
match value {
|
|
ColorOrTrue::Boolean(true) => {}
|
|
ColorOrTrue::Boolean(false) => {
|
|
return Err("Native layout overline face must be true or a color".to_owned());
|
|
}
|
|
ColorOrTrue::Color(color) => {
|
|
validate_style_string(color, "overline color")?;
|
|
}
|
|
}
|
|
}
|
|
if let Some(underline) = &self.underline {
|
|
if !underline.position {
|
|
return Err("Native layout underline position must be true".to_owned());
|
|
}
|
|
if let Some(color) = &underline.color {
|
|
validate_style_string(color, "underline color")?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn to_lisp(&self) -> String {
|
|
if let Some(lisp) = &self.lisp {
|
|
return lisp.clone();
|
|
}
|
|
let mut output = String::from("(");
|
|
let mut first = true;
|
|
if self.inherit.as_deref() == Some("default") {
|
|
push_property(&mut output, &mut first, ":inherit", "default");
|
|
}
|
|
if self.inverse_video == Some(true) {
|
|
push_property(&mut output, &mut first, ":inverse-video", "t");
|
|
}
|
|
if let Some(color) = &self.foreground {
|
|
let mut value = String::new();
|
|
push_lisp_string(&mut value, color);
|
|
push_property(&mut output, &mut first, ":foreground", &value);
|
|
}
|
|
if let Some(color) = &self.background {
|
|
let mut value = String::new();
|
|
push_lisp_string(&mut value, color);
|
|
push_property(&mut output, &mut first, ":background", &value);
|
|
}
|
|
if let Some(overline) = &self.overline {
|
|
let value = match overline {
|
|
ColorOrTrue::Boolean(true) => "t".to_owned(),
|
|
ColorOrTrue::Boolean(false) => unreachable!("validated face template"),
|
|
ColorOrTrue::Color(color) => {
|
|
let mut value = String::new();
|
|
push_lisp_string(&mut value, color);
|
|
value
|
|
}
|
|
};
|
|
push_property(&mut output, &mut first, ":overline", &value);
|
|
}
|
|
if let Some(underline) = &self.underline {
|
|
let mut value = String::from("(:position t");
|
|
if let Some(color) = &underline.color {
|
|
value.push_str(" :color ");
|
|
push_lisp_string(&mut value, color);
|
|
}
|
|
value.push(')');
|
|
push_property(&mut output, &mut first, ":underline", &value);
|
|
}
|
|
output.push(')');
|
|
output
|
|
}
|
|
}
|
|
|
|
impl StyleTemplate {
|
|
fn compile(&self) -> Result<CompiledStyle, String> {
|
|
self.face.validate()?;
|
|
Ok(CompiledStyle {
|
|
mode: self.mode,
|
|
face: self.face.to_lisp(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn composed_face(
|
|
properties: &AtomProperties,
|
|
styles: &[CompiledStyle],
|
|
) -> Result<Option<String>, String> {
|
|
if properties.style_ids.len() > MAX_TAPE_PROPERTY_ENTRIES {
|
|
return Err("Native layout tape has too many style layers".to_owned());
|
|
}
|
|
let mut faces: Vec<usize> = Vec::new();
|
|
let mut list_value = false;
|
|
for style_id in &properties.style_ids {
|
|
let style_index = *style_id as usize;
|
|
let style = styles
|
|
.get(style_index)
|
|
.ok_or_else(|| format!("Native layout tape has invalid style id {style_id}"))?;
|
|
match style.mode {
|
|
StyleMode::Set => {
|
|
faces.clear();
|
|
faces.push(style_index);
|
|
list_value = false;
|
|
}
|
|
StyleMode::Add => {
|
|
if !faces.is_empty() {
|
|
list_value = true;
|
|
}
|
|
faces.push(style_index);
|
|
}
|
|
}
|
|
}
|
|
if faces.is_empty() {
|
|
Ok(None)
|
|
} else if list_value {
|
|
Ok(Some(format!(
|
|
"({})",
|
|
faces
|
|
.iter()
|
|
.map(|index| format!("#{}#", index + 1))
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
)))
|
|
} else {
|
|
Ok(Some(format!("#{}#", faces[0] + 1)))
|
|
}
|
|
}
|
|
|
|
fn properties_to_lisp(
|
|
properties: Option<&AtomProperties>,
|
|
pixel_width: Option<u64>,
|
|
styles: &[CompiledStyle],
|
|
) -> Result<Option<String>, String> {
|
|
let mut output = String::from("(");
|
|
let mut first = true;
|
|
if let Some(properties) = properties {
|
|
if properties.owners.len() > MAX_TAPE_PROPERTY_ENTRIES
|
|
|| properties.roles.len() > MAX_TAPE_PROPERTY_ENTRIES
|
|
|| properties.property_template_ids.len() > MAX_TAPE_PROPERTY_ENTRIES
|
|
{
|
|
return Err("Native layout tape property record exceeds its entry limit".to_owned());
|
|
}
|
|
if let Some(face) = composed_face(properties, styles)? {
|
|
push_property(&mut output, &mut first, "face", &face);
|
|
}
|
|
if let Some(content) = properties.content {
|
|
push_property(
|
|
&mut output,
|
|
&mut first,
|
|
"ebox-content",
|
|
&content.to_string(),
|
|
);
|
|
}
|
|
if let Some(content_idx) = properties.content_idx {
|
|
push_property(
|
|
&mut output,
|
|
&mut first,
|
|
"ebox-content-idx",
|
|
&content_idx.to_string(),
|
|
);
|
|
}
|
|
if let Some(owner) = properties.owner {
|
|
push_property(
|
|
&mut output,
|
|
&mut first,
|
|
"ebox-content-owner",
|
|
&owner.to_string(),
|
|
);
|
|
}
|
|
if !properties.owners.is_empty() {
|
|
let owners = format!(
|
|
"({})",
|
|
properties
|
|
.owners
|
|
.iter()
|
|
.map(i64::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
);
|
|
push_property(&mut output, &mut first, "ebox-content-owners", &owners);
|
|
}
|
|
for role in REGION_ROLES {
|
|
if let Some(entry) = properties
|
|
.roles
|
|
.iter()
|
|
.rev()
|
|
.find(|entry| entry.role == role)
|
|
{
|
|
push_property(
|
|
&mut output,
|
|
&mut first,
|
|
role.lisp_property(),
|
|
&entry.region_id.to_string(),
|
|
);
|
|
}
|
|
}
|
|
if let Some(scroll_window) = properties.scroll_window {
|
|
push_property(
|
|
&mut output,
|
|
&mut first,
|
|
"ebox-scroll-window",
|
|
&scroll_window.to_string(),
|
|
);
|
|
}
|
|
if !properties.property_template_ids.is_empty() {
|
|
let template_ids = format!(
|
|
"({})",
|
|
properties
|
|
.property_template_ids
|
|
.iter()
|
|
.map(u32::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
);
|
|
push_property(
|
|
&mut output,
|
|
&mut first,
|
|
"ebox-native-property-template-ids",
|
|
&template_ids,
|
|
);
|
|
}
|
|
}
|
|
if let Some(pixel_width) = pixel_width {
|
|
push_property(
|
|
&mut output,
|
|
&mut first,
|
|
"display",
|
|
&format!("(space :width ({pixel_width}))"),
|
|
);
|
|
}
|
|
output.push(')');
|
|
if first {
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(output))
|
|
}
|
|
}
|
|
|
|
fn encode_lisp_literal(
|
|
text: &str,
|
|
spaces: &[TapeSpaceSpan],
|
|
property_spans: &[TapePropertySpan],
|
|
styles: &[CompiledStyle],
|
|
character_count: u64,
|
|
max_bytes: usize,
|
|
) -> Result<String, String> {
|
|
encode_lisp_literal_inner(
|
|
text,
|
|
spaces,
|
|
property_spans,
|
|
styles,
|
|
character_count,
|
|
max_bytes,
|
|
true,
|
|
)
|
|
}
|
|
|
|
fn encode_lisp_literal_inner(
|
|
text: &str,
|
|
spaces: &[TapeSpaceSpan],
|
|
property_spans: &[TapePropertySpan],
|
|
styles: &[CompiledStyle],
|
|
character_count: u64,
|
|
max_bytes: usize,
|
|
define_styles: bool,
|
|
) -> Result<String, String> {
|
|
let mut boundaries = Vec::with_capacity(2 + spaces.len() * 2 + property_spans.len() * 2);
|
|
boundaries.push(0);
|
|
boundaries.push(character_count);
|
|
for space in spaces {
|
|
boundaries.push(space.start);
|
|
boundaries.push(space.start + 1);
|
|
}
|
|
for span in property_spans {
|
|
boundaries.push(span.start);
|
|
boundaries.push(span.end);
|
|
}
|
|
boundaries.sort_unstable();
|
|
boundaries.dedup();
|
|
|
|
let mut intervals: Vec<LiteralInterval> = Vec::new();
|
|
let mut property_index = 0;
|
|
let mut space_index = 0;
|
|
for boundary in boundaries.windows(2) {
|
|
let start = boundary[0];
|
|
let end = boundary[1];
|
|
if start == end {
|
|
continue;
|
|
}
|
|
while property_index < property_spans.len() && property_spans[property_index].end <= start {
|
|
property_index += 1;
|
|
}
|
|
while space_index < spaces.len() && spaces[space_index].start < start {
|
|
space_index += 1;
|
|
}
|
|
let properties = property_spans
|
|
.get(property_index)
|
|
.filter(|span| span.start <= start && end <= span.end)
|
|
.map(|span| &span.properties);
|
|
let pixel_width = spaces
|
|
.get(space_index)
|
|
.filter(|space| space.start == start)
|
|
.map(|space| space.width);
|
|
if let Some(properties) = properties_to_lisp(properties, pixel_width, styles)? {
|
|
if let Some(previous) = intervals.last_mut() {
|
|
if previous.end == start && previous.properties == properties {
|
|
previous.end = end;
|
|
continue;
|
|
}
|
|
}
|
|
intervals.push(LiteralInterval {
|
|
start,
|
|
end,
|
|
properties,
|
|
});
|
|
}
|
|
}
|
|
|
|
let mut shared_properties = shared_literal_properties(&intervals, styles.len());
|
|
let mut literal = String::with_capacity(text.len().saturating_mul(2));
|
|
literal.push_str("#(");
|
|
push_lisp_string(&mut literal, text);
|
|
if define_styles {
|
|
for (index, style) in styles.iter().enumerate() {
|
|
literal.push_str(" 0 0 (face #");
|
|
literal.push_str(&(index + 1).to_string());
|
|
literal.push('=');
|
|
literal.push_str(&style.face);
|
|
literal.push(')');
|
|
}
|
|
}
|
|
for interval in intervals {
|
|
literal.push(' ');
|
|
literal.push_str(&interval.start.to_string());
|
|
literal.push(' ');
|
|
literal.push_str(&interval.end.to_string());
|
|
literal.push(' ');
|
|
if let Some(label) = shared_properties.get_mut(&interval.properties) {
|
|
if !label.defined {
|
|
literal.push('#');
|
|
literal.push_str(&label.id.to_string());
|
|
literal.push('=');
|
|
literal.push_str(&interval.properties);
|
|
label.defined = true;
|
|
} else {
|
|
literal.push('#');
|
|
literal.push_str(&label.id.to_string());
|
|
literal.push('#');
|
|
}
|
|
} else {
|
|
literal.push_str(&interval.properties);
|
|
}
|
|
if literal.len() > max_bytes {
|
|
return Err("Native layout tape exceeds its byte limit".to_owned());
|
|
}
|
|
}
|
|
literal.push(')');
|
|
if literal.len() > max_bytes {
|
|
return Err("Native layout tape exceeds its byte limit".to_owned());
|
|
}
|
|
Ok(literal)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SharedLiteralProperty {
|
|
id: usize,
|
|
defined: bool,
|
|
}
|
|
|
|
fn shared_literal_properties(
|
|
intervals: &[LiteralInterval],
|
|
reserved_label_count: usize,
|
|
) -> BTreeMap<String, SharedLiteralProperty> {
|
|
let mut counts = BTreeMap::new();
|
|
for interval in intervals {
|
|
*counts.entry(interval.properties.clone()).or_insert(0_usize) += 1;
|
|
}
|
|
let mut labels = BTreeMap::new();
|
|
let mut next_label = reserved_label_count + 1;
|
|
for interval in intervals {
|
|
if labels.contains_key(&interval.properties) {
|
|
continue;
|
|
}
|
|
let count = *counts.get(&interval.properties).unwrap_or(&0);
|
|
if count < 2 {
|
|
continue;
|
|
}
|
|
let id = next_label;
|
|
let definition_overhead = format!("#{id}=").len();
|
|
let reference_length = format!("#{id}#").len();
|
|
let saved_references = count - 1;
|
|
let saved_bytes = saved_references
|
|
.saturating_mul(interval.properties.len().saturating_sub(reference_length));
|
|
if saved_bytes > definition_overhead {
|
|
labels.insert(
|
|
interval.properties.clone(),
|
|
SharedLiteralProperty { id, defined: false },
|
|
);
|
|
next_label += 1;
|
|
}
|
|
}
|
|
labels
|
|
}
|
|
|
|
fn tape_properties(mut properties: AtomProperties, complete: bool) -> AtomProperties {
|
|
if !complete {
|
|
properties.content = None;
|
|
properties.content_idx = None;
|
|
properties.owner = None;
|
|
properties.owners.clear();
|
|
properties.roles.clear();
|
|
properties.scroll_window = None;
|
|
properties.property_template_ids.clear();
|
|
}
|
|
properties
|
|
}
|
|
|
|
fn tape_properties_empty(properties: &AtomProperties) -> bool {
|
|
properties.style_ids.is_empty()
|
|
&& properties.content.is_none()
|
|
&& properties.content_idx.is_none()
|
|
&& properties.owner.is_none()
|
|
&& properties.owners.is_empty()
|
|
&& properties.roles.is_empty()
|
|
&& properties.scroll_window.is_none()
|
|
&& properties.property_template_ids.is_empty()
|
|
}
|
|
|
|
fn push_property_span(
|
|
spans: &mut Vec<TapePropertySpan>,
|
|
start: u64,
|
|
end: u64,
|
|
properties: AtomProperties,
|
|
complete: bool,
|
|
) {
|
|
let properties = tape_properties(properties, complete);
|
|
if start == end || tape_properties_empty(&properties) {
|
|
return;
|
|
}
|
|
if let Some(previous) = spans.last_mut() {
|
|
if previous.end == start && previous.properties == properties {
|
|
previous.end = end;
|
|
return;
|
|
}
|
|
}
|
|
spans.push(TapePropertySpan {
|
|
start,
|
|
end,
|
|
properties,
|
|
});
|
|
}
|
|
|
|
fn push_metadata_span(
|
|
records: &mut Vec<TapeMetadataRecord>,
|
|
last_by_key: &mut BTreeMap<(u8, i64, u32), usize>,
|
|
kind: u8,
|
|
region_id: i64,
|
|
index: u32,
|
|
start: u64,
|
|
end: u64,
|
|
) -> Result<(), String> {
|
|
if start >= end {
|
|
return Ok(());
|
|
}
|
|
if region_id <= 0 {
|
|
return Err("Native layout tape metadata has an invalid region id".to_owned());
|
|
}
|
|
let key = (kind, region_id, index);
|
|
if let Some(previous_index) = last_by_key.get(&key).copied() {
|
|
let previous = &mut records[previous_index];
|
|
if previous.end == start {
|
|
previous.end = end;
|
|
return Ok(());
|
|
}
|
|
}
|
|
if records.len() >= MAX_TAPE_METADATA_RECORDS {
|
|
return Err("Native layout tape has too many metadata records".to_owned());
|
|
}
|
|
let record_index = records.len();
|
|
records.push(TapeMetadataRecord {
|
|
kind,
|
|
region_id,
|
|
index,
|
|
start,
|
|
end,
|
|
});
|
|
last_by_key.insert(key, record_index);
|
|
Ok(())
|
|
}
|
|
|
|
fn metadata_extent_region_ids(properties: &AtomProperties) -> Vec<i64> {
|
|
let mut region_ids = properties.owners.clone();
|
|
if let Some(region_id) = properties.owner {
|
|
if !region_ids.contains(®ion_id) {
|
|
region_ids.push(region_id);
|
|
}
|
|
}
|
|
if let Some(region_id) = properties.content {
|
|
if !region_ids.contains(®ion_id) {
|
|
region_ids.push(region_id);
|
|
}
|
|
}
|
|
for role in &properties.roles {
|
|
if !region_ids.contains(&role.region_id) {
|
|
region_ids.push(role.region_id);
|
|
}
|
|
}
|
|
region_ids
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct MetadataExtentRange {
|
|
start: u64,
|
|
end: u64,
|
|
last_event: u64,
|
|
last_safe_event: u64,
|
|
interleaved: bool,
|
|
}
|
|
|
|
fn push_box_extent_metadata_records(
|
|
text: &str,
|
|
property_spans: &[TapePropertySpan],
|
|
records: &mut Vec<TapeMetadataRecord>,
|
|
last_by_key: &mut BTreeMap<(u8, i64, u32), usize>,
|
|
) -> Result<(), String> {
|
|
let mut non_newline_prefix = Vec::with_capacity(text.chars().count() + 1);
|
|
non_newline_prefix.push(0_u64);
|
|
for character in text.chars() {
|
|
let previous = *non_newline_prefix.last().unwrap_or(&0);
|
|
non_newline_prefix.push(previous + u64::from(character != '\n'));
|
|
}
|
|
let mut ranges: BTreeMap<i64, MetadataExtentRange> = BTreeMap::new();
|
|
let mut safe_events: BTreeMap<i64, u64> = BTreeMap::new();
|
|
let mut event = 0_u64;
|
|
for span in property_spans {
|
|
let region_ids = metadata_extent_region_ids(&span.properties);
|
|
for region_id in ®ion_ids {
|
|
let region_id = *region_id;
|
|
if region_id <= 0 {
|
|
return Err("Native layout tape metadata has an invalid extent id".to_owned());
|
|
}
|
|
if let Some(range) = ranges.get_mut(®ion_id) {
|
|
let safe_event = safe_events.get(®ion_id).copied().unwrap_or(0);
|
|
if span.start > range.end
|
|
&& event - range.last_event > safe_event - range.last_safe_event
|
|
{
|
|
range.interleaved = true;
|
|
}
|
|
range.start = range.start.min(span.start);
|
|
range.end = range.end.max(span.end);
|
|
} else {
|
|
ranges.insert(
|
|
region_id,
|
|
MetadataExtentRange {
|
|
start: span.start,
|
|
end: span.end,
|
|
last_event: event,
|
|
last_safe_event: safe_events.get(®ion_id).copied().unwrap_or(0),
|
|
interleaved: false,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
let start = usize::try_from(span.start)
|
|
.map_err(|_| "Native layout tape metadata span is too large".to_owned())?;
|
|
let end = usize::try_from(span.end)
|
|
.map_err(|_| "Native layout tape metadata span is too large".to_owned())?;
|
|
let contains_non_newline = non_newline_prefix
|
|
.get(end)
|
|
.zip(non_newline_prefix.get(start))
|
|
.is_some_and(|(end_count, start_count)| end_count > start_count);
|
|
if contains_non_newline && !region_ids.is_empty() {
|
|
event += 1;
|
|
let mut safe_region_ids = span.properties.owners.clone();
|
|
if region_ids.len() == 1 && !safe_region_ids.contains(®ion_ids[0]) {
|
|
safe_region_ids.push(region_ids[0]);
|
|
}
|
|
for region_id in safe_region_ids {
|
|
*safe_events.entry(region_id).or_insert(0) += 1;
|
|
}
|
|
}
|
|
for region_id in region_ids {
|
|
let range = ranges
|
|
.get_mut(®ion_id)
|
|
.ok_or_else(|| "Native layout tape extent state disappeared".to_owned())?;
|
|
range.last_event = event;
|
|
range.last_safe_event = safe_events.get(®ion_id).copied().unwrap_or(0);
|
|
}
|
|
}
|
|
for (region_id, range) in ranges {
|
|
if !range.interleaved {
|
|
push_metadata_span(
|
|
records,
|
|
last_by_key,
|
|
METADATA_BOX_EXTENT,
|
|
region_id,
|
|
0,
|
|
range.start,
|
|
range.end,
|
|
)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn build_root_metadata_records(
|
|
text: &str,
|
|
property_spans: &[TapePropertySpan],
|
|
) -> Result<Vec<TapeMetadataRecord>, String> {
|
|
let mut records = Vec::new();
|
|
let mut last_by_key = BTreeMap::new();
|
|
|
|
push_box_extent_metadata_records(text, property_spans, &mut records, &mut last_by_key)?;
|
|
|
|
// Root replacement role templates use one-based buffer points. Keep the
|
|
// tape zero-based and translate only after the sidecar has been validated
|
|
// by Emacs.
|
|
for span in property_spans {
|
|
if let Some(region_id) = span.properties.content {
|
|
push_metadata_span(
|
|
&mut records,
|
|
&mut last_by_key,
|
|
METADATA_ROLE_CONTENT,
|
|
region_id,
|
|
0,
|
|
span.start,
|
|
span.end,
|
|
)?;
|
|
}
|
|
if let Some(region_id) = span.properties.owner {
|
|
push_metadata_span(
|
|
&mut records,
|
|
&mut last_by_key,
|
|
METADATA_ROLE_CONTENT_OWNER,
|
|
region_id,
|
|
0,
|
|
span.start,
|
|
span.end,
|
|
)?;
|
|
}
|
|
for role in REGION_ROLES {
|
|
if let Some(entry) = span
|
|
.properties
|
|
.roles
|
|
.iter()
|
|
.rev()
|
|
.find(|entry| entry.role == role)
|
|
{
|
|
push_metadata_span(
|
|
&mut records,
|
|
&mut last_by_key,
|
|
role.metadata_kind(),
|
|
entry.region_id,
|
|
0,
|
|
span.start,
|
|
span.end,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Direct scroll content spans precede wrapper-owner line spans, matching
|
|
// `ebox--build-scroll-content-span-template'. Extra non-scroll region ids
|
|
// are harmless: installation filters against the live scroll-state table.
|
|
for span in property_spans {
|
|
if let (Some(region_id), Some(index)) =
|
|
(span.properties.content, span.properties.content_idx)
|
|
{
|
|
let index = u32::try_from(index)
|
|
.map_err(|_| "Native layout tape has an invalid content index".to_owned())?;
|
|
push_metadata_span(
|
|
&mut records,
|
|
&mut last_by_key,
|
|
METADATA_SCROLL_CONTENT,
|
|
region_id,
|
|
index,
|
|
span.start,
|
|
span.end,
|
|
)?;
|
|
}
|
|
}
|
|
|
|
let mut line_ranges = Vec::new();
|
|
let mut line_start = 0_u64;
|
|
let mut position = 0_u64;
|
|
for character in text.chars() {
|
|
if character == '\n' {
|
|
if line_start < position {
|
|
line_ranges.push((line_start, position));
|
|
}
|
|
line_start = position + 1;
|
|
}
|
|
position += 1;
|
|
}
|
|
if line_start < position {
|
|
line_ranges.push((line_start, position));
|
|
}
|
|
|
|
let mut owner_line_indexes: BTreeMap<i64, u32> = BTreeMap::new();
|
|
let mut first_span = 0_usize;
|
|
for (line_start, line_end) in line_ranges {
|
|
while first_span < property_spans.len() && property_spans[first_span].end <= line_start {
|
|
first_span += 1;
|
|
}
|
|
let mut line_owners: BTreeMap<i64, (u64, u64)> = BTreeMap::new();
|
|
let mut span_index = first_span;
|
|
while span_index < property_spans.len() && property_spans[span_index].start < line_end {
|
|
let span = &property_spans[span_index];
|
|
let start = span.start.max(line_start);
|
|
let end = span.end.min(line_end);
|
|
if start < end {
|
|
let mut owner_ids = span.properties.owners.clone();
|
|
if let Some(owner) = span.properties.owner {
|
|
if !owner_ids.contains(&owner) {
|
|
owner_ids.push(owner);
|
|
}
|
|
}
|
|
for region_id in owner_ids {
|
|
if region_id <= 0 {
|
|
return Err(
|
|
"Native layout tape metadata has an invalid owner id".to_owned()
|
|
);
|
|
}
|
|
line_owners
|
|
.entry(region_id)
|
|
.and_modify(|record| record.1 = record.1.max(end))
|
|
.or_insert((start, end));
|
|
}
|
|
}
|
|
span_index += 1;
|
|
}
|
|
for (region_id, (start, end)) in line_owners {
|
|
let index = owner_line_indexes.entry(region_id).or_insert(0);
|
|
push_metadata_span(
|
|
&mut records,
|
|
&mut last_by_key,
|
|
METADATA_SCROLL_OWNER,
|
|
region_id,
|
|
*index,
|
|
start,
|
|
end,
|
|
)?;
|
|
*index = index
|
|
.checked_add(1)
|
|
.ok_or_else(|| "Native layout tape owner index overflow".to_owned())?;
|
|
}
|
|
}
|
|
|
|
if let Some((region_id, start, end)) = property_spans.iter().find_map(|span| {
|
|
span.properties
|
|
.scroll_window
|
|
.map(|region_id| (region_id, span.start, span.end))
|
|
}) {
|
|
push_metadata_span(
|
|
&mut records,
|
|
&mut last_by_key,
|
|
METADATA_SCROLL_WINDOW,
|
|
region_id,
|
|
0,
|
|
start,
|
|
end,
|
|
)?;
|
|
}
|
|
Ok(records)
|
|
}
|
|
|
|
fn push_fragment_role(
|
|
roles: &mut Vec<(&'static str, i64)>,
|
|
role: &'static str,
|
|
region_id: i64,
|
|
) -> Result<(), String> {
|
|
if region_id <= 0 {
|
|
return Err("Native layout fragment has an invalid region id".to_owned());
|
|
}
|
|
if !roles
|
|
.iter()
|
|
.any(|(existing_role, existing_id)| *existing_role == role && *existing_id == region_id)
|
|
{
|
|
roles.push((role, region_id));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn fragment_roles(properties: &AtomProperties) -> Result<Vec<(&'static str, i64)>, String> {
|
|
let mut roles = Vec::new();
|
|
for region_id in &properties.owners {
|
|
push_fragment_role(&mut roles, "content-owner", *region_id)?;
|
|
}
|
|
if let Some(region_id) = properties.content {
|
|
push_fragment_role(&mut roles, "content", region_id)?;
|
|
}
|
|
if let Some(region_id) = properties.owner {
|
|
push_fragment_role(&mut roles, "content-owner", region_id)?;
|
|
}
|
|
for role in REGION_ROLES {
|
|
if let Some(entry) = properties
|
|
.roles
|
|
.iter()
|
|
.rev()
|
|
.find(|entry| entry.role == role)
|
|
{
|
|
let symbol = metadata_role_symbol(role.metadata_kind())
|
|
.ok_or_else(|| "Native layout fragment has an unknown role".to_owned())?;
|
|
push_fragment_role(&mut roles, symbol, entry.region_id)?;
|
|
}
|
|
}
|
|
Ok(roles)
|
|
}
|
|
|
|
fn build_fragment_templates(
|
|
text: &str,
|
|
spaces: &[TapeSpaceSpan],
|
|
property_spans: &[TapePropertySpan],
|
|
) -> Result<Vec<FragmentTemplate>, String> {
|
|
let character_count = u64::try_from(text.chars().count())
|
|
.map_err(|_| "Native layout fragment character count overflow".to_owned())?;
|
|
let mut boundaries = Vec::with_capacity(2 + spaces.len() * 2 + property_spans.len() * 2);
|
|
boundaries.push(0);
|
|
boundaries.push(character_count);
|
|
for space in spaces {
|
|
boundaries.push(space.start);
|
|
boundaries.push(space.start + 1);
|
|
}
|
|
for span in property_spans {
|
|
boundaries.push(span.start);
|
|
boundaries.push(span.end);
|
|
}
|
|
boundaries.sort_unstable();
|
|
boundaries.dedup();
|
|
|
|
let mut line_prefix = Vec::with_capacity(text.chars().count() + 1);
|
|
line_prefix.push(0_u64);
|
|
for character in text.chars() {
|
|
let previous = *line_prefix.last().unwrap_or(&0);
|
|
line_prefix.push(previous + u64::from(character == '\n'));
|
|
}
|
|
|
|
let empty = AtomProperties::default();
|
|
let mut property_index = 0;
|
|
let mut fragments = Vec::with_capacity(boundaries.len().saturating_sub(1));
|
|
for boundary in boundaries.windows(2) {
|
|
let start = boundary[0];
|
|
let end = boundary[1];
|
|
if start == end {
|
|
continue;
|
|
}
|
|
while property_index < property_spans.len() && property_spans[property_index].end <= start {
|
|
property_index += 1;
|
|
}
|
|
let properties = property_spans
|
|
.get(property_index)
|
|
.filter(|span| span.start <= start && end <= span.end)
|
|
.map_or(&empty, |span| &span.properties);
|
|
let line_index = usize::try_from(start)
|
|
.map_err(|_| "Native layout fragment offset is too large".to_owned())?;
|
|
let line = *line_prefix
|
|
.get(line_index)
|
|
.ok_or_else(|| "Native layout fragment line offset is invalid".to_owned())?;
|
|
if properties.content_idx.is_some_and(|index| index < 0) {
|
|
return Err("Native layout fragment has a negative content index".to_owned());
|
|
}
|
|
fragments.push(FragmentTemplate {
|
|
start,
|
|
end,
|
|
line,
|
|
roles: fragment_roles(properties)?,
|
|
content_owner: properties.owner,
|
|
content_index: properties.content_idx,
|
|
property_template_ids: properties.property_template_ids.clone(),
|
|
style_ids: properties.style_ids.clone(),
|
|
});
|
|
}
|
|
Ok(fragments)
|
|
}
|
|
|
|
struct RootMetadataPayload {
|
|
literal: String,
|
|
record_count: usize,
|
|
fragments: Vec<FragmentTemplate>,
|
|
fragment_bytes: Vec<u8>,
|
|
fragment_count: usize,
|
|
}
|
|
|
|
fn metadata_role_symbol(kind: u8) -> Option<&'static str> {
|
|
match kind {
|
|
METADATA_ROLE_CONTENT => Some("content"),
|
|
METADATA_ROLE_CONTENT_OWNER => Some("content-owner"),
|
|
METADATA_ROLE_PADDING_TOP => Some("pt"),
|
|
METADATA_ROLE_PADDING_BOTTOM => Some("pb"),
|
|
METADATA_ROLE_PADDING_LEFT => Some("pl"),
|
|
METADATA_ROLE_PADDING_RIGHT => Some("pr"),
|
|
METADATA_ROLE_MARGIN_TOP => Some("mt"),
|
|
METADATA_ROLE_MARGIN_BOTTOM => Some("mb"),
|
|
METADATA_ROLE_MARGIN_LEFT => Some("ml"),
|
|
METADATA_ROLE_MARGIN_RIGHT => Some("mr"),
|
|
METADATA_ROLE_BORDER_TOP => Some("bt"),
|
|
METADATA_ROLE_BORDER_BOTTOM => Some("bb"),
|
|
METADATA_ROLE_BORDER_LEFT => Some("bl"),
|
|
METADATA_ROLE_BORDER_RIGHT => Some("br"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn push_hash_table(output: &mut String, entries: Vec<String>) {
|
|
output.push_str("#s(hash-table test equal data (");
|
|
output.push_str(&entries.join(" "));
|
|
output.push_str("))");
|
|
}
|
|
|
|
fn metadata_literal_from_records(records: &[TapeMetadataRecord]) -> Result<String, String> {
|
|
let mut role_table: BTreeMap<(i64, &'static str), Vec<(u64, u64)>> = BTreeMap::new();
|
|
let mut extent_table: BTreeMap<i64, (u64, u64)> = BTreeMap::new();
|
|
let mut scroll_table: BTreeMap<i64, Vec<(u32, u64, u64)>> = BTreeMap::new();
|
|
let mut scroll_window_p = false;
|
|
|
|
for record in records {
|
|
if let Some(role) = metadata_role_symbol(record.kind) {
|
|
if record.index != 0 {
|
|
return Err("Native layout tape role metadata has a line index".to_owned());
|
|
}
|
|
role_table
|
|
.entry((record.region_id, role))
|
|
.or_default()
|
|
.push((record.start + 1, record.end + 1));
|
|
} else if record.kind == METADATA_BOX_EXTENT {
|
|
if record.index != 0 {
|
|
return Err("Native layout tape box extent metadata has a line index".to_owned());
|
|
}
|
|
extent_table
|
|
.entry(record.region_id)
|
|
.and_modify(|extent| {
|
|
extent.0 = extent.0.min(record.start + 1);
|
|
extent.1 = extent.1.max(record.end + 1);
|
|
})
|
|
.or_insert((record.start + 1, record.end + 1));
|
|
} else if record.kind == METADATA_SCROLL_CONTENT || record.kind == METADATA_SCROLL_OWNER {
|
|
scroll_table.entry(record.region_id).or_default().push((
|
|
record.index,
|
|
record.start,
|
|
record.end,
|
|
));
|
|
} else if record.kind == METADATA_SCROLL_WINDOW {
|
|
if record.index != 0 {
|
|
return Err("Native layout tape scroll-window metadata has a line index".to_owned());
|
|
}
|
|
scroll_window_p = true;
|
|
} else {
|
|
return Err("Native layout tape has unknown metadata kind".to_owned());
|
|
}
|
|
}
|
|
|
|
let role_entries = role_table
|
|
.into_iter()
|
|
.map(|((region_id, role), spans)| {
|
|
let spans = spans
|
|
.into_iter()
|
|
.map(|(start, end)| format!("({start} . {end})"))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
format!("({region_id} {role}) ({spans})")
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let extent_entries = extent_table
|
|
.into_iter()
|
|
.map(|(region_id, (start, end))| format!("{region_id} ({start} . {end})"))
|
|
.collect::<Vec<_>>();
|
|
let scroll_entries = scroll_table
|
|
.into_iter()
|
|
.map(|(region_id, spans)| {
|
|
let spans = spans
|
|
.into_iter()
|
|
.map(|(index, start, end)| format!("({index} {start} . {end})"))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
format!("{region_id} ({spans})")
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut literal = String::from("(:prepared-p t :role-span-template ");
|
|
push_hash_table(&mut literal, role_entries);
|
|
literal.push_str(" :box-extent-template ");
|
|
push_hash_table(&mut literal, extent_entries);
|
|
literal.push_str(" :scroll-content-span-template ");
|
|
push_hash_table(&mut literal, scroll_entries);
|
|
literal.push_str(" :scroll-window-p ");
|
|
literal.push_str(if scroll_window_p { "t" } else { "nil" });
|
|
literal.push(')');
|
|
Ok(literal)
|
|
}
|
|
|
|
fn push_fragment_u32(output: &mut Vec<u8>, value: usize, label: &str) -> Result<(), String> {
|
|
output.extend_from_slice(
|
|
&u32::try_from(value)
|
|
.map_err(|_| format!("Native layout fragment has too many {label}"))?
|
|
.to_le_bytes(),
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn encode_fragment_templates(fragments: &[FragmentTemplate]) -> Result<Vec<u8>, String> {
|
|
let mut output = Vec::new();
|
|
for fragment in fragments {
|
|
output.extend_from_slice(&fragment.start.to_le_bytes());
|
|
output.extend_from_slice(&fragment.end.to_le_bytes());
|
|
output.extend_from_slice(&fragment.line.to_le_bytes());
|
|
output.extend_from_slice(&fragment.content_owner.unwrap_or(i64::MIN).to_le_bytes());
|
|
output.extend_from_slice(&fragment.content_index.unwrap_or(i64::MIN).to_le_bytes());
|
|
push_fragment_u32(&mut output, fragment.roles.len(), "roles")?;
|
|
push_fragment_u32(
|
|
&mut output,
|
|
fragment.property_template_ids.len(),
|
|
"property templates",
|
|
)?;
|
|
push_fragment_u32(&mut output, fragment.style_ids.len(), "styles")?;
|
|
output.extend_from_slice(&0_u32.to_le_bytes());
|
|
for (role, region_id) in &fragment.roles {
|
|
let kind = match *role {
|
|
"content" => METADATA_ROLE_CONTENT,
|
|
"content-owner" => METADATA_ROLE_CONTENT_OWNER,
|
|
"pt" => METADATA_ROLE_PADDING_TOP,
|
|
"pb" => METADATA_ROLE_PADDING_BOTTOM,
|
|
"pl" => METADATA_ROLE_PADDING_LEFT,
|
|
"pr" => METADATA_ROLE_PADDING_RIGHT,
|
|
"mt" => METADATA_ROLE_MARGIN_TOP,
|
|
"mb" => METADATA_ROLE_MARGIN_BOTTOM,
|
|
"ml" => METADATA_ROLE_MARGIN_LEFT,
|
|
"mr" => METADATA_ROLE_MARGIN_RIGHT,
|
|
"bt" => METADATA_ROLE_BORDER_TOP,
|
|
"bb" => METADATA_ROLE_BORDER_BOTTOM,
|
|
"bl" => METADATA_ROLE_BORDER_LEFT,
|
|
"br" => METADATA_ROLE_BORDER_RIGHT,
|
|
_ => return Err("Native layout fragment has an unknown role".to_owned()),
|
|
};
|
|
output.push(kind);
|
|
output.extend_from_slice(®ion_id.to_le_bytes());
|
|
}
|
|
for template_id in &fragment.property_template_ids {
|
|
output.extend_from_slice(&template_id.to_le_bytes());
|
|
}
|
|
for style_id in &fragment.style_ids {
|
|
output.extend_from_slice(&style_id.to_le_bytes());
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|
|
|
|
fn root_metadata_payload(
|
|
text: &str,
|
|
spaces: &[TapeSpaceSpan],
|
|
property_spans: &[TapePropertySpan],
|
|
complete: bool,
|
|
root_metadata: bool,
|
|
max_bytes: usize,
|
|
) -> Result<Option<RootMetadataPayload>, String> {
|
|
if !complete {
|
|
return Ok(None);
|
|
}
|
|
let records = if root_metadata {
|
|
build_root_metadata_records(text, property_spans)?
|
|
} else {
|
|
property_spans
|
|
.iter()
|
|
.find_map(|span| {
|
|
span.properties.scroll_window.map(|region_id| {
|
|
vec![TapeMetadataRecord {
|
|
kind: METADATA_SCROLL_WINDOW,
|
|
region_id,
|
|
index: 0,
|
|
start: span.start,
|
|
end: span.end,
|
|
}]
|
|
})
|
|
})
|
|
.unwrap_or_default()
|
|
};
|
|
let mut fragments = build_fragment_templates(text, spaces, property_spans)?;
|
|
let propagated_roles = propagated_fragment_roles(&fragments);
|
|
for (fragment, roles) in fragments.iter_mut().zip(propagated_roles) {
|
|
fragment.roles = roles;
|
|
}
|
|
if records.is_empty() && fragments.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
let literal = metadata_literal_from_records(&records)?;
|
|
let fragment_bytes = encode_fragment_templates(&fragments)?;
|
|
let fragment_count = fragments.len();
|
|
if literal
|
|
.len()
|
|
.checked_add(fragment_bytes.len())
|
|
.is_none_or(|length| length > max_bytes)
|
|
{
|
|
return Err("Native layout tape metadata exceeds its byte limit".to_owned());
|
|
}
|
|
Ok(Some(RootMetadataPayload {
|
|
literal,
|
|
record_count: records.len(),
|
|
fragments,
|
|
fragment_bytes,
|
|
fragment_count,
|
|
}))
|
|
}
|
|
|
|
fn flatten_layout_tape(tape: LayoutTape, complete: bool) -> Result<FlatLayoutTape, String> {
|
|
if tape.lines.is_empty() {
|
|
return Err("Native layout tape has no lines".to_owned());
|
|
}
|
|
let line_count = tape.lines.len();
|
|
let mut characters = Vec::new();
|
|
for (line_index, line) in tape.lines.into_iter().enumerate() {
|
|
let line_width = tape_width(line.width, "line width")?;
|
|
let expected_break = line_index + 1 < line_count;
|
|
if line.break_after.is_some() != expected_break {
|
|
return Err("Native layout tape break invariant failed".to_owned());
|
|
}
|
|
let mut encoded_width = 0_u64;
|
|
for atom in line.atoms {
|
|
match atom {
|
|
TapeAtom::Text {
|
|
text,
|
|
width,
|
|
properties,
|
|
} => {
|
|
let width = tape_width(width, "text width")?;
|
|
if text.is_empty() || text.contains('\n') {
|
|
return Err("Native layout tape has invalid text atom".to_owned());
|
|
}
|
|
let properties = tape_properties(properties, complete);
|
|
characters.extend(text.chars().map(|value| TapeCharacter {
|
|
value,
|
|
pixel_width: None,
|
|
properties: properties.clone(),
|
|
}));
|
|
encoded_width = encoded_width
|
|
.checked_add(width)
|
|
.ok_or_else(|| "Native layout tape width overflow".to_owned())?;
|
|
}
|
|
TapeAtom::Space { width, properties } => {
|
|
let width = tape_width(width, "space width")?;
|
|
if width == 0 {
|
|
return Err("Native layout tape has empty pixel space".to_owned());
|
|
}
|
|
characters.push(TapeCharacter {
|
|
value: ' ',
|
|
pixel_width: Some(width),
|
|
properties: tape_properties(properties, complete),
|
|
});
|
|
encoded_width = encoded_width
|
|
.checked_add(width)
|
|
.ok_or_else(|| "Native layout tape width overflow".to_owned())?;
|
|
}
|
|
}
|
|
}
|
|
if encoded_width != line_width {
|
|
return Err("Native layout tape line width invariant failed".to_owned());
|
|
}
|
|
if let Some(properties) = line.break_after {
|
|
characters.push(TapeCharacter {
|
|
value: '\n',
|
|
pixel_width: None,
|
|
properties: tape_properties(properties, complete),
|
|
});
|
|
}
|
|
}
|
|
Ok(FlatLayoutTape {
|
|
style_count: tape.style_count,
|
|
line_count: count_u32(line_count, "lines")?,
|
|
characters,
|
|
})
|
|
}
|
|
|
|
fn tape_commit_batch(old: &[TapeCharacter], new: &[TapeCharacter]) -> CommitBatch {
|
|
diff_commit_batch(
|
|
0,
|
|
1,
|
|
old,
|
|
new,
|
|
|left, right| left.value == right.value,
|
|
|left, right| left.pixel_width == right.pixel_width && left.properties == right.properties,
|
|
)
|
|
.expect("fixed consecutive adapter revisions")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn minimal_tape_patches(old: &[TapeCharacter], new: &[TapeCharacter]) -> Vec<TapePatch> {
|
|
tape_commit_batch(old, new).semantic_edits
|
|
}
|
|
|
|
fn tape_character_encoding_parts(
|
|
characters: &[TapeCharacter],
|
|
) -> Result<(String, Vec<TapeSpaceSpan>, Vec<TapePropertySpan>), String> {
|
|
let mut text = String::new();
|
|
let mut spaces = Vec::new();
|
|
let mut property_spans = Vec::new();
|
|
for (position, character) in characters.iter().enumerate() {
|
|
let start = u64::try_from(position)
|
|
.map_err(|_| "Native layout tape character count overflow".to_owned())?;
|
|
text.push(character.value);
|
|
if let Some(width) = character.pixel_width {
|
|
spaces.push(TapeSpaceSpan { start, width });
|
|
}
|
|
push_property_span(
|
|
&mut property_spans,
|
|
start,
|
|
start + 1,
|
|
character.properties.clone(),
|
|
true,
|
|
);
|
|
}
|
|
Ok((text, spaces, property_spans))
|
|
}
|
|
|
|
fn encode_patch_replacement_body(
|
|
target: &[TapeCharacter],
|
|
patches: &[TapePatch],
|
|
styles: &[CompiledStyle],
|
|
max_bytes: usize,
|
|
) -> Result<Vec<u8>, String> {
|
|
let replacement_count = patches.iter().try_fold(0_usize, |count, patch| {
|
|
count
|
|
.checked_add(patch.new_end - patch.new_start)
|
|
.ok_or_else(|| "Native layout patch replacement count overflow".to_owned())
|
|
})?;
|
|
let mut replacement_characters = Vec::with_capacity(replacement_count);
|
|
for patch in patches {
|
|
replacement_characters.extend_from_slice(&target[patch.new_start..patch.new_end]);
|
|
}
|
|
let (text, spaces, property_spans) = tape_character_encoding_parts(&replacement_characters)?;
|
|
let character_count = u64::try_from(replacement_count)
|
|
.map_err(|_| "Native layout patch replacement count overflow".to_owned())?;
|
|
encode_lisp_literal_inner(
|
|
&text,
|
|
&spaces,
|
|
&property_spans,
|
|
styles,
|
|
character_count,
|
|
max_bytes,
|
|
true,
|
|
)
|
|
.map(String::into_bytes)
|
|
}
|
|
|
|
fn encode_patch_combined_payload(
|
|
replacement: &[u8],
|
|
metadata: Option<&RootMetadataPayload>,
|
|
fragment_style_delta: Option<&FragmentStyleDelta>,
|
|
max_bytes: usize,
|
|
) -> Result<Vec<u8>, String> {
|
|
let metadata_literal = metadata.map_or("nil", |payload| payload.literal.as_str());
|
|
let style_delta_literal = fragment_style_delta.map(|delta| {
|
|
let entries = delta
|
|
.iter()
|
|
.map(|(index, style_ids)| {
|
|
let ids = style_ids
|
|
.iter()
|
|
.map(u32::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
if ids.is_empty() {
|
|
format!("({index})")
|
|
} else {
|
|
format!("({index} {ids})")
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
format!("({entries})")
|
|
});
|
|
let payload_len = 1_usize
|
|
.checked_add(replacement.len())
|
|
.and_then(|size| size.checked_add(1))
|
|
.and_then(|size| size.checked_add(metadata_literal.len()))
|
|
.and_then(|size| {
|
|
style_delta_literal
|
|
.as_ref()
|
|
.map_or(Some(size), |literal| size.checked_add(1 + literal.len()))
|
|
})
|
|
.and_then(|size| size.checked_add(1))
|
|
.ok_or_else(|| "Native layout patch payload size overflow".to_owned())?;
|
|
if payload_len > max_bytes {
|
|
return Err("Native layout patch exceeds its byte limit".to_owned());
|
|
}
|
|
let mut payload = Vec::with_capacity(payload_len);
|
|
payload.push(b'[');
|
|
payload.extend_from_slice(replacement);
|
|
payload.push(b' ');
|
|
payload.extend_from_slice(metadata_literal.as_bytes());
|
|
if let Some(literal) = style_delta_literal {
|
|
payload.push(b' ');
|
|
payload.extend_from_slice(literal.as_bytes());
|
|
}
|
|
payload.push(b']');
|
|
Ok(payload)
|
|
}
|
|
|
|
/// Emacs adapter payload built on the editor-independent core batch.
|
|
struct EmacsCommitBatch {
|
|
core: CommitBatch,
|
|
combined_payload: Vec<u8>,
|
|
metadata_records: usize,
|
|
fragment_bytes: Vec<u8>,
|
|
fragment_records: usize,
|
|
reuse_fragment_template: bool,
|
|
reuse_ownership_template: bool,
|
|
reuse_mount_projection: bool,
|
|
fragment_style_delta: Option<FragmentStyleDelta>,
|
|
}
|
|
|
|
fn build_emacs_commit_batch(
|
|
old: &FlatLayoutTape,
|
|
target: &FlatLayoutTape,
|
|
compiled_styles: &[CompiledStyle],
|
|
complete: bool,
|
|
root_metadata: bool,
|
|
max_bytes: usize,
|
|
) -> Result<EmacsCommitBatch, String> {
|
|
let core = tape_commit_batch(&old.characters, &target.characters);
|
|
let descriptor_bytes = core
|
|
.semantic_edits
|
|
.len()
|
|
.checked_add(core.coordinate_edits.len())
|
|
.ok_or_else(|| "Native layout patch descriptor count overflow".to_owned())?
|
|
.checked_mul(32)
|
|
.ok_or_else(|| "Native layout patch size overflow".to_owned())?;
|
|
let fixed_body_bytes = 56_usize
|
|
.checked_add(descriptor_bytes)
|
|
.ok_or_else(|| "Native layout patch size overflow".to_owned())?;
|
|
let payload_limit = max_bytes
|
|
.checked_sub(TAPE_HEADER_LEN)
|
|
.and_then(|remaining| remaining.checked_sub(fixed_body_bytes))
|
|
.ok_or_else(|| "Native layout patch exceeds its byte limit".to_owned())?;
|
|
let (target_text, target_spaces, target_property_spans) =
|
|
tape_character_encoding_parts(&target.characters)?;
|
|
let mut metadata_payload = root_metadata_payload(
|
|
&target_text,
|
|
&target_spaces,
|
|
&target_property_spans,
|
|
complete,
|
|
root_metadata,
|
|
payload_limit,
|
|
)?;
|
|
let old_fragments = if metadata_payload.is_some() {
|
|
let (old_text, old_spaces, old_property_spans) =
|
|
tape_character_encoding_parts(&old.characters)?;
|
|
let mut fragments = build_fragment_templates(&old_text, &old_spaces, &old_property_spans)?;
|
|
let roles = propagated_fragment_roles(&fragments);
|
|
for (fragment, roles) in fragments.iter_mut().zip(roles) {
|
|
fragment.roles = roles;
|
|
}
|
|
Some(fragments)
|
|
} else {
|
|
None
|
|
};
|
|
let reuse_fragment_template = old_fragments
|
|
.as_ref()
|
|
.zip(metadata_payload.as_ref())
|
|
.is_some_and(|(old, target)| *old == target.fragments);
|
|
let reuse_ownership_template = old_fragments
|
|
.as_ref()
|
|
.zip(metadata_payload.as_ref())
|
|
.is_some_and(|(old, target)| {
|
|
fragment_ownership_spans(old) == fragment_ownership_spans(&target.fragments)
|
|
});
|
|
let reuse_mount_projection = old_fragments
|
|
.as_ref()
|
|
.zip(metadata_payload.as_ref())
|
|
.is_some_and(|(old, target)| {
|
|
fragment_region_mount_projection(old)
|
|
== fragment_region_mount_projection(&target.fragments)
|
|
});
|
|
let fragment_style_delta = if reuse_fragment_template {
|
|
None
|
|
} else {
|
|
old_fragments
|
|
.as_ref()
|
|
.zip(metadata_payload.as_ref())
|
|
.and_then(|(old, target)| fragment_style_delta(old, &target.fragments))
|
|
};
|
|
let replacements = encode_patch_replacement_body(
|
|
&target.characters,
|
|
&core.semantic_edits,
|
|
compiled_styles,
|
|
payload_limit,
|
|
)?;
|
|
let combined_payload = encode_patch_combined_payload(
|
|
&replacements,
|
|
metadata_payload.as_ref(),
|
|
fragment_style_delta.as_ref(),
|
|
payload_limit,
|
|
)?;
|
|
let metadata_records = metadata_payload
|
|
.as_ref()
|
|
.map_or(0, |payload| payload.record_count);
|
|
let fragment_records = if reuse_fragment_template || fragment_style_delta.is_some() {
|
|
0
|
|
} else {
|
|
metadata_payload
|
|
.as_ref()
|
|
.map_or(0, |payload| payload.fragment_count)
|
|
};
|
|
let fragment_bytes = if reuse_fragment_template || fragment_style_delta.is_some() {
|
|
Vec::new()
|
|
} else {
|
|
metadata_payload.as_mut().map_or_else(Vec::new, |payload| {
|
|
std::mem::take(&mut payload.fragment_bytes)
|
|
})
|
|
};
|
|
Ok(EmacsCommitBatch {
|
|
core,
|
|
combined_payload,
|
|
metadata_records,
|
|
fragment_bytes,
|
|
fragment_records,
|
|
reuse_fragment_template,
|
|
reuse_ownership_template,
|
|
reuse_mount_projection,
|
|
fragment_style_delta,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn encode_layout_patch_tape(
|
|
old_tape: LayoutTape,
|
|
target_tape: LayoutTape,
|
|
styles: &[StyleTemplate],
|
|
identity: TapeIdentity,
|
|
root_metadata: bool,
|
|
max_bytes: usize,
|
|
) -> Result<Vec<u8>, String> {
|
|
let old = flatten_layout_tape(old_tape, identity.complete)?;
|
|
let target = flatten_layout_tape(target_tape, identity.complete)?;
|
|
if old.style_count != target.style_count || target.style_count as usize != styles.len() {
|
|
return Err("Native layout patch style table mismatch".to_owned());
|
|
}
|
|
let compiled_styles = styles
|
|
.iter()
|
|
.map(StyleTemplate::compile)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let batch = build_emacs_commit_batch(
|
|
&old,
|
|
&target,
|
|
&compiled_styles,
|
|
identity.complete,
|
|
root_metadata,
|
|
max_bytes,
|
|
)?;
|
|
let patch_count = count_u32(batch.core.semantic_edits.len(), "patches")?;
|
|
let coordinate_patch_count =
|
|
count_u32(batch.core.coordinate_edits.len(), "coordinate patches")?;
|
|
let target_character_count = u64::try_from(target.characters.len())
|
|
.map_err(|_| "Native layout patch character count overflow".to_owned())?;
|
|
let base_character_count = u64::try_from(old.characters.len())
|
|
.map_err(|_| "Native layout patch base character count overflow".to_owned())?;
|
|
let mut writer = TapeWriter::new(max_bytes)?;
|
|
writer.push_u64(base_character_count)?;
|
|
writer.push_u32(patch_count)?;
|
|
// Publication remains a minimal text/property patch. The compact target
|
|
// role sidecar lets Emacs swap exact runtime indexes without synchronously
|
|
// rescanning the page or rebuilding thousands of markers per frame.
|
|
writer.push_u32(
|
|
u32::from(batch.reuse_fragment_template)
|
|
| (u32::from(batch.reuse_ownership_template) << 1)
|
|
| (u32::from(batch.reuse_mount_projection) << 2)
|
|
| (u32::from(batch.fragment_style_delta.is_some()) << 3),
|
|
)?;
|
|
writer.push_u64(
|
|
u64::try_from(batch.metadata_records)
|
|
.map_err(|_| "Native layout patch has too many metadata records".to_owned())?,
|
|
)?;
|
|
writer.push_u64(
|
|
u64::try_from(batch.combined_payload.len())
|
|
.map_err(|_| "Native layout patch body is too large".to_owned())?,
|
|
)?;
|
|
writer.push_u64(
|
|
u64::try_from(batch.fragment_bytes.len())
|
|
.map_err(|_| "Native layout fragment tape is too large".to_owned())?,
|
|
)?;
|
|
writer.push_u64(
|
|
u64::try_from(batch.fragment_records)
|
|
.map_err(|_| "Native layout fragment tape has too many records".to_owned())?,
|
|
)?;
|
|
writer.push_u32(coordinate_patch_count)?;
|
|
writer.push_u32(0)?;
|
|
for patch in &batch.core.semantic_edits {
|
|
writer.push_u64(patch.old_start as u64)?;
|
|
writer.push_u64(patch.old_end as u64)?;
|
|
writer.push_u64(patch.new_start as u64)?;
|
|
writer.push_u64(patch.new_end as u64)?;
|
|
}
|
|
for patch in &batch.core.coordinate_edits {
|
|
writer.push_u64(patch.old_start as u64)?;
|
|
writer.push_u64(patch.old_end as u64)?;
|
|
writer.push_u64(patch.new_start as u64)?;
|
|
writer.push_u64(patch.new_end as u64)?;
|
|
}
|
|
writer.push_bytes(&batch.combined_payload)?;
|
|
writer.push_bytes(&batch.fragment_bytes)?;
|
|
Ok(writer.finish_header(
|
|
identity,
|
|
true,
|
|
true,
|
|
target.style_count,
|
|
target.line_count,
|
|
target_character_count,
|
|
))
|
|
}
|
|
|
|
pub(crate) fn encode_layout_tape(
|
|
tape: LayoutTape,
|
|
styles: &[StyleTemplate],
|
|
identity: TapeIdentity,
|
|
root_metadata: bool,
|
|
max_bytes: usize,
|
|
) -> Result<Vec<u8>, String> {
|
|
if tape.lines.is_empty() {
|
|
return Err("Native layout tape has no lines".to_owned());
|
|
}
|
|
let line_count = count_u32(tape.lines.len(), "lines")?;
|
|
if tape.style_count as usize != styles.len() {
|
|
return Err("Native layout tape style table mismatch".to_owned());
|
|
}
|
|
let compiled_styles = styles
|
|
.iter()
|
|
.map(StyleTemplate::compile)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let mut text = String::new();
|
|
let mut line_widths = Vec::with_capacity(tape.lines.len());
|
|
let mut spaces = Vec::new();
|
|
let mut property_spans = Vec::new();
|
|
let mut character_count = 0_u64;
|
|
|
|
for (line_index, line) in tape.lines.into_iter().enumerate() {
|
|
let line_width = tape_width(line.width, "line width")?;
|
|
line_widths.push(line_width);
|
|
let expected_break = line_index + 1 < line_count as usize;
|
|
if line.break_after.is_some() != expected_break {
|
|
return Err("Native layout tape break invariant failed".to_owned());
|
|
}
|
|
let mut encoded_width = 0_u64;
|
|
for atom in line.atoms {
|
|
match atom {
|
|
TapeAtom::Text {
|
|
text: atom_text,
|
|
width,
|
|
properties,
|
|
} => {
|
|
let width = tape_width(width, "text width")?;
|
|
if atom_text.is_empty() || atom_text.contains('\n') {
|
|
return Err("Native layout tape has invalid text atom".to_owned());
|
|
}
|
|
let start = character_count;
|
|
text.push_str(&atom_text);
|
|
character_count = character_count
|
|
.checked_add(u64::try_from(atom_text.chars().count()).map_err(|_| {
|
|
"Native layout tape character count overflow".to_owned()
|
|
})?)
|
|
.ok_or_else(|| "Native layout tape character overflow".to_owned())?;
|
|
push_property_span(
|
|
&mut property_spans,
|
|
start,
|
|
character_count,
|
|
properties,
|
|
identity.complete,
|
|
);
|
|
encoded_width = encoded_width
|
|
.checked_add(width)
|
|
.ok_or_else(|| "Native layout tape width overflow".to_owned())?;
|
|
}
|
|
TapeAtom::Space { width, properties } => {
|
|
let width = tape_width(width, "space width")?;
|
|
if width == 0 {
|
|
return Err("Native layout tape has empty pixel space".to_owned());
|
|
}
|
|
let start = character_count;
|
|
text.push(' ');
|
|
spaces.push(TapeSpaceSpan { start, width });
|
|
character_count = character_count
|
|
.checked_add(1)
|
|
.ok_or_else(|| "Native layout tape character overflow".to_owned())?;
|
|
push_property_span(
|
|
&mut property_spans,
|
|
start,
|
|
character_count,
|
|
properties,
|
|
identity.complete,
|
|
);
|
|
encoded_width = encoded_width
|
|
.checked_add(width)
|
|
.ok_or_else(|| "Native layout tape width overflow".to_owned())?;
|
|
}
|
|
}
|
|
}
|
|
if encoded_width != line_width {
|
|
return Err("Native layout tape line width invariant failed".to_owned());
|
|
}
|
|
if let Some(properties) = line.break_after {
|
|
let start = character_count;
|
|
text.push('\n');
|
|
character_count = character_count
|
|
.checked_add(1)
|
|
.ok_or_else(|| "Native layout tape character overflow".to_owned())?;
|
|
push_property_span(
|
|
&mut property_spans,
|
|
start,
|
|
character_count,
|
|
properties,
|
|
identity.complete,
|
|
);
|
|
}
|
|
}
|
|
|
|
let fixed_body_bytes = 40_usize
|
|
.checked_add(
|
|
(line_count as usize)
|
|
.checked_mul(8)
|
|
.ok_or_else(|| "Native layout tape size overflow".to_owned())?,
|
|
)
|
|
.ok_or_else(|| "Native layout tape size overflow".to_owned())?;
|
|
let metadata_limit = max_bytes
|
|
.checked_sub(TAPE_HEADER_LEN)
|
|
.and_then(|remaining| remaining.checked_sub(fixed_body_bytes))
|
|
.ok_or_else(|| "Native layout tape exceeds its byte limit".to_owned())?;
|
|
let metadata_payload = root_metadata_payload(
|
|
&text,
|
|
&spaces,
|
|
&property_spans,
|
|
identity.complete,
|
|
root_metadata,
|
|
metadata_limit,
|
|
)?;
|
|
let metadata_bytes = metadata_payload
|
|
.as_ref()
|
|
.map_or(0, |payload| payload.literal.len());
|
|
let metadata_records = metadata_payload
|
|
.as_ref()
|
|
.map_or(0, |payload| payload.record_count);
|
|
let fragment_bytes = metadata_payload
|
|
.as_ref()
|
|
.map_or(0, |payload| payload.fragment_bytes.len());
|
|
let fragment_records = metadata_payload
|
|
.as_ref()
|
|
.map_or(0, |payload| payload.fragment_count);
|
|
let literal_limit = max_bytes
|
|
.checked_sub(TAPE_HEADER_LEN)
|
|
.and_then(|remaining| remaining.checked_sub(fixed_body_bytes))
|
|
.and_then(|remaining| remaining.checked_sub(metadata_bytes))
|
|
.and_then(|remaining| remaining.checked_sub(fragment_bytes))
|
|
.ok_or_else(|| "Native layout tape exceeds its byte limit".to_owned())?;
|
|
let literal = encode_lisp_literal(
|
|
&text,
|
|
&spaces,
|
|
&property_spans,
|
|
&compiled_styles,
|
|
character_count,
|
|
literal_limit,
|
|
)?;
|
|
|
|
let mut writer = TapeWriter::new(max_bytes)?;
|
|
writer.push_u64(
|
|
u64::try_from(literal.len())
|
|
.map_err(|_| "Native layout tape literal is too large".to_owned())?,
|
|
)?;
|
|
writer.push_u32(line_count)?;
|
|
writer.push_u32(
|
|
u32::try_from(metadata_bytes)
|
|
.map_err(|_| "Native layout tape metadata is too large".to_owned())?,
|
|
)?;
|
|
writer.push_u64(
|
|
u64::try_from(metadata_records)
|
|
.map_err(|_| "Native layout tape has too many metadata records".to_owned())?,
|
|
)?;
|
|
writer.push_u64(
|
|
u64::try_from(fragment_bytes)
|
|
.map_err(|_| "Native layout fragment tape is too large".to_owned())?,
|
|
)?;
|
|
writer.push_u64(
|
|
u64::try_from(fragment_records)
|
|
.map_err(|_| "Native layout fragment tape has too many records".to_owned())?,
|
|
)?;
|
|
for width in line_widths {
|
|
writer.push_u64(width)?;
|
|
}
|
|
writer.push_bytes(literal.as_bytes())?;
|
|
if let Some(payload) = metadata_payload {
|
|
writer.push_bytes(payload.literal.as_bytes())?;
|
|
writer.push_bytes(&payload.fragment_bytes)?;
|
|
}
|
|
|
|
Ok(writer.finish_header(
|
|
identity,
|
|
true,
|
|
false,
|
|
tape.style_count,
|
|
line_count,
|
|
character_count,
|
|
))
|
|
}
|
|
|
|
pub fn encode_error_tape(identity: TapeIdentity, message: &str, max_bytes: usize) -> Vec<u8> {
|
|
let limit = max_bytes.max(MIN_TAPE_BYTES);
|
|
let mut writer = TapeWriter::new(limit).expect("minimum tape limit");
|
|
let capacity = limit - TAPE_HEADER_LEN - 4;
|
|
let mut length = message.len().min(capacity);
|
|
while !message.is_char_boundary(length) {
|
|
length -= 1;
|
|
}
|
|
let message = &message.as_bytes()[..length];
|
|
writer
|
|
.push_u32(u32::try_from(message.len()).unwrap_or(u32::MAX))
|
|
.expect("bounded error length");
|
|
writer.push_bytes(message).expect("bounded error body");
|
|
writer.finish_header(identity, false, false, 0, 0, 0)
|
|
}
|
|
|
|
impl LayoutDocument {
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
if self.version != LAYOUT_VERSION {
|
|
return Err(format!(
|
|
"Unsupported native layout IR version {}",
|
|
self.version
|
|
));
|
|
}
|
|
if self.space_width <= 0 {
|
|
return Err("Native layout space width must be positive".to_owned());
|
|
}
|
|
if self.styles.len() != self.style_count as usize {
|
|
return Err("Native layout style table length mismatch".to_owned());
|
|
}
|
|
if self.styles.len() > MAX_TAPE_PROPERTY_ENTRIES {
|
|
return Err("Native layout style table exceeds its entry limit".to_owned());
|
|
}
|
|
if self.property_template_count > MAX_PROPERTY_TEMPLATE_COUNT {
|
|
return Err("Native layout property template table exceeds its entry limit".to_owned());
|
|
}
|
|
for style in &self.styles {
|
|
style.face.validate()?;
|
|
}
|
|
let mut nodes = 0;
|
|
let mut work_units = 0;
|
|
validate_node(
|
|
&self.root,
|
|
0,
|
|
&mut nodes,
|
|
&mut work_units,
|
|
self.style_count,
|
|
self.property_template_count,
|
|
)
|
|
}
|
|
|
|
pub fn validate_context(&self, context: LayoutContext) -> Result<(), String> {
|
|
let mut work_units = 0;
|
|
add_context_work(&self.root, context, &mut work_units)
|
|
}
|
|
|
|
pub(crate) fn layout_tape(
|
|
&self,
|
|
context: LayoutContext,
|
|
root_width_override: Option<i64>,
|
|
) -> Result<LayoutTape, String> {
|
|
if root_width_override.is_some() && !matches!(self.root, LayoutNode::Box { .. }) {
|
|
Err("Native root width override requires a box root".to_owned())
|
|
} else {
|
|
render_node_with_override(
|
|
&self.root,
|
|
context,
|
|
false,
|
|
root_width_override.map(|declared_width| BoxOverride {
|
|
declared_width: Some(declared_width),
|
|
..BoxOverride::default()
|
|
}),
|
|
)
|
|
.map(|rendered| rendered.into_tape(self.style_count))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_nonnegative(name: &str, value: i64) -> Result<(), String> {
|
|
if value < 0 {
|
|
Err(format!("Native layout {name} cannot be negative"))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn validate_dimension(name: &str, value: i64) -> Result<(), String> {
|
|
validate_nonnegative(name, value)?;
|
|
if value > MAX_LAYOUT_DIMENSION {
|
|
Err(format!("Native layout {name} exceeds the dimension limit"))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn add_work_units(work_units: &mut usize, amount: usize) -> Result<(), String> {
|
|
*work_units = work_units
|
|
.checked_add(amount)
|
|
.ok_or_else(|| "Native layout work estimate overflowed".to_owned())?;
|
|
if *work_units > MAX_LAYOUT_WORK_UNITS {
|
|
Err("Native layout exceeds the work-unit limit".to_owned())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn add_vertical_size_work(size: &Size, work_units: &mut usize) -> Result<(), String> {
|
|
match size {
|
|
Size::Lines { value } => {
|
|
add_work_units(work_units, usize::try_from(*value).unwrap_or(usize::MAX))
|
|
}
|
|
Size::FitContent { limit: Some(limit) } => add_vertical_size_work(limit, work_units),
|
|
Size::Add { values } | Size::Subtract { values } => {
|
|
for value in &values.0 {
|
|
add_vertical_size_work(value, work_units)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn add_context_size_work(
|
|
size: &Size,
|
|
context: LayoutContext,
|
|
work_units: &mut usize,
|
|
) -> Result<(), String> {
|
|
match size {
|
|
Size::ViewportHeight => add_work_units(
|
|
work_units,
|
|
usize::try_from(context.viewport_height).unwrap_or(usize::MAX),
|
|
),
|
|
Size::FitContent { limit: Some(limit) } => {
|
|
add_context_size_work(limit, context, work_units)
|
|
}
|
|
Size::Add { values } | Size::Subtract { values } => {
|
|
for value in &values.0 {
|
|
add_context_size_work(value, context, work_units)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn add_context_work(
|
|
node: &LayoutNode,
|
|
context: LayoutContext,
|
|
work_units: &mut usize,
|
|
) -> Result<(), String> {
|
|
match node {
|
|
LayoutNode::Box {
|
|
child,
|
|
height,
|
|
min_height,
|
|
max_height,
|
|
..
|
|
} => {
|
|
for size in [height, min_height, max_height] {
|
|
add_context_size_work(size, context, work_units)?;
|
|
}
|
|
if let Some(child) = child {
|
|
add_context_work(child, context, work_units)?;
|
|
}
|
|
}
|
|
LayoutNode::Row { children } | LayoutNode::Column { children } => {
|
|
for child in children {
|
|
add_context_work(child, context, work_units)?;
|
|
}
|
|
}
|
|
LayoutNode::Flex { height, items, .. } => {
|
|
add_context_size_work(height, context, work_units)?;
|
|
for item in items {
|
|
add_context_work(&item.node, context, work_units)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_size_at_depth(name: &str, size: &Size, depth: usize) -> Result<(), String> {
|
|
if depth > MAX_LAYOUT_DEPTH {
|
|
return Err(format!(
|
|
"Native layout {name} expression exceeds the depth limit"
|
|
));
|
|
}
|
|
match size {
|
|
Size::Pixels { value } | Size::Lines { value } => validate_dimension(name, *value),
|
|
Size::FitContent { limit: Some(limit) } => validate_size_at_depth(name, limit, depth + 1),
|
|
Size::Add { values } | Size::Subtract { values } => {
|
|
if values.0.is_empty() {
|
|
return Err(format!("Native layout {name} expression cannot be empty"));
|
|
}
|
|
for value in &values.0 {
|
|
validate_size_at_depth(name, value, depth + 1)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn validate_size(name: &str, size: &Size) -> Result<(), String> {
|
|
validate_size_at_depth(name, size, 0)
|
|
}
|
|
|
|
fn validate_node(
|
|
node: &LayoutNode,
|
|
depth: usize,
|
|
nodes: &mut usize,
|
|
work_units: &mut usize,
|
|
style_count: u32,
|
|
property_template_count: u32,
|
|
) -> Result<(), String> {
|
|
if depth > MAX_LAYOUT_DEPTH {
|
|
return Err("Native layout tree exceeds the depth limit".to_owned());
|
|
}
|
|
*nodes += 1;
|
|
if *nodes > MAX_LAYOUT_NODES {
|
|
return Err("Native layout tree exceeds the node limit".to_owned());
|
|
}
|
|
add_work_units(work_units, 1)?;
|
|
match node {
|
|
LayoutNode::Box {
|
|
region_id,
|
|
content,
|
|
child,
|
|
content_width_exact: _,
|
|
content_min_width,
|
|
width,
|
|
min_width,
|
|
max_width,
|
|
height,
|
|
min_height,
|
|
max_height,
|
|
padding_left,
|
|
padding_right,
|
|
padding_top,
|
|
padding_bottom,
|
|
margin_left,
|
|
margin_right,
|
|
margin_top,
|
|
margin_bottom,
|
|
border_left,
|
|
border_right,
|
|
foreground_style,
|
|
background_style,
|
|
border_left_style,
|
|
border_right_style,
|
|
border_top_style,
|
|
border_bottom_style,
|
|
surface_template_id,
|
|
scroll_offset,
|
|
..
|
|
} => {
|
|
if *region_id <= 0 {
|
|
return Err("Native layout region id must be positive".to_owned());
|
|
}
|
|
if let Some(content_min_width) = content_min_width {
|
|
validate_dimension("content-min-width", *content_min_width)?;
|
|
}
|
|
if content.is_some() == child.is_some() {
|
|
return Err(
|
|
"Native layout box must contain exactly one text or child value".to_owned(),
|
|
);
|
|
}
|
|
for (name, size) in [
|
|
("width", width),
|
|
("min-width", min_width),
|
|
("max-width", max_width),
|
|
("height", height),
|
|
("min-height", min_height),
|
|
("max-height", max_height),
|
|
] {
|
|
validate_size(name, size)?;
|
|
}
|
|
for size in [height, min_height, max_height] {
|
|
add_vertical_size_work(size, work_units)?;
|
|
}
|
|
for (name, value) in [
|
|
("padding-left", *padding_left),
|
|
("padding-right", *padding_right),
|
|
("padding-top", *padding_top),
|
|
("padding-bottom", *padding_bottom),
|
|
("margin-left", *margin_left),
|
|
("margin-right", *margin_right),
|
|
("margin-top", *margin_top),
|
|
("margin-bottom", *margin_bottom),
|
|
("border-left", *border_left),
|
|
("border-right", *border_right),
|
|
("scroll-offset", *scroll_offset),
|
|
] {
|
|
validate_dimension(name, value)?;
|
|
}
|
|
add_work_units(
|
|
work_units,
|
|
usize::try_from(padding_top + padding_bottom + margin_top + margin_bottom)
|
|
.unwrap_or(usize::MAX),
|
|
)?;
|
|
for style_id in [
|
|
foreground_style,
|
|
background_style,
|
|
border_left_style,
|
|
border_right_style,
|
|
border_top_style,
|
|
border_bottom_style,
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
{
|
|
if *style_id >= style_count {
|
|
return Err("Native layout style id exceeds the style table".to_owned());
|
|
}
|
|
}
|
|
if let Some(template_id) = surface_template_id {
|
|
if *template_id >= property_template_count {
|
|
return Err(
|
|
"Native layout property template id exceeds the property template table"
|
|
.to_owned(),
|
|
);
|
|
}
|
|
}
|
|
if let Some(text) = content {
|
|
if text.lines.is_empty() {
|
|
return Err("Native layout measured text must contain one line".to_owned());
|
|
}
|
|
add_work_units(work_units, text.lines.len())?;
|
|
for line in &text.lines {
|
|
add_work_units(work_units, line.clusters.len())?;
|
|
for cluster in &line.clusters {
|
|
if cluster.text.is_empty() {
|
|
return Err("Native layout cluster text cannot be empty".to_owned());
|
|
}
|
|
validate_dimension("cluster width", cluster.width)?;
|
|
if let Some(template_id) = cluster.source_template_id {
|
|
if template_id >= property_template_count {
|
|
return Err(
|
|
"Native layout property template id exceeds the property template table"
|
|
.to_owned(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(child) = child {
|
|
validate_node(
|
|
child,
|
|
depth + 1,
|
|
nodes,
|
|
work_units,
|
|
style_count,
|
|
property_template_count,
|
|
)?;
|
|
}
|
|
}
|
|
LayoutNode::Row { children } | LayoutNode::Column { children } => {
|
|
if children.is_empty() {
|
|
return Err("Native layout container must contain a child".to_owned());
|
|
}
|
|
for child in children {
|
|
validate_node(
|
|
child,
|
|
depth + 1,
|
|
nodes,
|
|
work_units,
|
|
style_count,
|
|
property_template_count,
|
|
)?;
|
|
}
|
|
}
|
|
LayoutNode::Flex {
|
|
width,
|
|
height,
|
|
row_gap,
|
|
column_gap,
|
|
items,
|
|
..
|
|
} => {
|
|
validate_size("flex width", width)?;
|
|
validate_size("flex height", height)?;
|
|
add_vertical_size_work(height, work_units)?;
|
|
validate_dimension("flex row gap", *row_gap)?;
|
|
validate_dimension("flex column gap", *column_gap)?;
|
|
if items.is_empty() {
|
|
return Err("Native flex container must contain an item".to_owned());
|
|
}
|
|
add_work_units(
|
|
work_units,
|
|
usize::try_from(*row_gap)
|
|
.unwrap_or(usize::MAX)
|
|
.saturating_mul(items.len()),
|
|
)?;
|
|
for item in items {
|
|
if !item.grow.is_finite()
|
|
|| item.grow < 0.0
|
|
|| !item.shrink.is_finite()
|
|
|| item.shrink < 0.0
|
|
{
|
|
return Err("Native flex factors must be finite and nonnegative".to_owned());
|
|
}
|
|
validate_size("flex basis", &item.basis)?;
|
|
validate_node(
|
|
&item.node,
|
|
depth + 1,
|
|
nodes,
|
|
work_units,
|
|
style_count,
|
|
property_template_count,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn box_sizing_content_width(
|
|
value: i64,
|
|
sizing: BoxSizing,
|
|
padding_left: i64,
|
|
padding_right: i64,
|
|
border_left: i64,
|
|
border_right: i64,
|
|
) -> i64 {
|
|
if sizing == BoxSizing::BorderBox {
|
|
(value - padding_left - padding_right - border_left - border_right).max(0)
|
|
} else {
|
|
value
|
|
}
|
|
}
|
|
|
|
fn box_sizing_content_height(
|
|
value: i64,
|
|
sizing: BoxSizing,
|
|
padding_top: i64,
|
|
padding_bottom: i64,
|
|
) -> i64 {
|
|
if sizing == BoxSizing::BorderBox {
|
|
(value - padding_top - padding_bottom).max(0)
|
|
} else {
|
|
value
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_width(
|
|
size: &Size,
|
|
fallback: Option<i64>,
|
|
context: LayoutContext,
|
|
sizing: BoxSizing,
|
|
padding_left: i64,
|
|
padding_right: i64,
|
|
border_left: i64,
|
|
border_right: i64,
|
|
stretch: Option<i64>,
|
|
min_content: i64,
|
|
max_content: i64,
|
|
) -> Option<i64> {
|
|
let convert = |value| {
|
|
box_sizing_content_width(
|
|
value,
|
|
sizing,
|
|
padding_left,
|
|
padding_right,
|
|
border_left,
|
|
border_right,
|
|
)
|
|
};
|
|
match size {
|
|
Size::Auto => fallback,
|
|
Size::Content => Some(max_content),
|
|
Size::None => None,
|
|
Size::Pixels { value } => Some(convert(*value)),
|
|
Size::Viewport => context
|
|
.viewport_width_known
|
|
.then(|| convert(context.viewport_width))
|
|
.or(fallback),
|
|
Size::MinContent => Some(min_content),
|
|
Size::MaxContent => Some(max_content),
|
|
Size::FitContent { limit } => {
|
|
let available = limit
|
|
.as_deref()
|
|
.and_then(|limit| {
|
|
resolve_width(
|
|
limit,
|
|
None,
|
|
context,
|
|
sizing,
|
|
padding_left,
|
|
padding_right,
|
|
border_left,
|
|
border_right,
|
|
stretch,
|
|
min_content,
|
|
max_content,
|
|
)
|
|
})
|
|
.or(stretch)
|
|
.unwrap_or(max_content);
|
|
Some(min_content.max(max_content.min(available)))
|
|
}
|
|
Size::Stretch | Size::Contain => stretch.or(fallback),
|
|
Size::Lines { .. } | Size::ViewportHeight | Size::Add { .. } | Size::Subtract { .. } => {
|
|
fallback
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_definite_width(
|
|
size: &Size,
|
|
context: LayoutContext,
|
|
sizing: BoxSizing,
|
|
padding_left: i64,
|
|
padding_right: i64,
|
|
border_left: i64,
|
|
border_right: i64,
|
|
stretch: Option<i64>,
|
|
) -> Option<i64> {
|
|
let convert = |value| {
|
|
box_sizing_content_width(
|
|
value,
|
|
sizing,
|
|
padding_left,
|
|
padding_right,
|
|
border_left,
|
|
border_right,
|
|
)
|
|
};
|
|
match size {
|
|
Size::Pixels { value } => Some(convert(*value)),
|
|
Size::Viewport => context
|
|
.viewport_width_known
|
|
.then(|| convert(context.viewport_width)),
|
|
Size::Stretch | Size::Contain | Size::FitContent { limit: None } => stretch,
|
|
Size::FitContent { limit: Some(limit) } => resolve_definite_width(
|
|
limit,
|
|
context,
|
|
sizing,
|
|
padding_left,
|
|
padding_right,
|
|
border_left,
|
|
border_right,
|
|
stretch,
|
|
),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_child_viewport_width(
|
|
width: &Size,
|
|
min_width: &Size,
|
|
max_width: &Size,
|
|
context: LayoutContext,
|
|
sizing: BoxSizing,
|
|
padding_left: i64,
|
|
padding_right: i64,
|
|
border_left: i64,
|
|
border_right: i64,
|
|
stretch: Option<i64>,
|
|
) -> Option<i64> {
|
|
let definite = |size| {
|
|
resolve_definite_width(
|
|
size,
|
|
context,
|
|
sizing,
|
|
padding_left,
|
|
padding_right,
|
|
border_left,
|
|
border_right,
|
|
stretch,
|
|
)
|
|
};
|
|
let minimum = definite(min_width).unwrap_or(0);
|
|
let maximum = definite(max_width);
|
|
let auto_stretch = if matches!(width, Size::Auto) {
|
|
stretch
|
|
} else {
|
|
None
|
|
};
|
|
let preferred = definite(width).or(auto_stretch).or(maximum);
|
|
preferred.map(|preferred| minimum.max(preferred.max(0).min(maximum.unwrap_or(i64::MAX))))
|
|
}
|
|
|
|
fn resolve_raw_height(size: &Size, context: LayoutContext) -> Option<i64> {
|
|
fn resolve(size: &Size, context: LayoutContext) -> Option<i128> {
|
|
match size {
|
|
Size::Lines { value } | Size::Pixels { value } => Some(i128::from(*value)),
|
|
Size::ViewportHeight => {
|
|
(context.viewport_height > 0).then_some(i128::from(context.viewport_height))
|
|
}
|
|
Size::Add { values } => values.0.iter().try_fold(0_i128, |total, value| {
|
|
resolve(value, context).and_then(|value| total.checked_add(value))
|
|
}),
|
|
Size::Subtract { values } => {
|
|
let mut values = values.0.iter();
|
|
let first = resolve(values.next()?, context)?;
|
|
if values.len() == 0 {
|
|
first.checked_neg()
|
|
} else {
|
|
values.try_fold(first, |total, value| {
|
|
resolve(value, context).and_then(|value| total.checked_sub(value))
|
|
})
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
resolve(size, context).map(|value| value.clamp(i64::MIN as i128, i64::MAX as i128) as i64)
|
|
}
|
|
|
|
fn resolve_height(
|
|
size: &Size,
|
|
fallback: Option<i64>,
|
|
context: LayoutContext,
|
|
sizing: BoxSizing,
|
|
padding_top: i64,
|
|
padding_bottom: i64,
|
|
) -> Option<i64> {
|
|
match size {
|
|
Size::Auto | Size::None | Size::Content => fallback,
|
|
Size::Lines { .. }
|
|
| Size::Pixels { .. }
|
|
| Size::ViewportHeight
|
|
| Size::Add { .. }
|
|
| Size::Subtract { .. } => resolve_raw_height(size, context).map(|value| {
|
|
box_sizing_content_height(value.max(0), sizing, padding_top, padding_bottom)
|
|
}),
|
|
_ => fallback,
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_definite_box_content_width(
|
|
width: &Size,
|
|
min_width: &Size,
|
|
max_width: &Size,
|
|
context: LayoutContext,
|
|
sizing: BoxSizing,
|
|
padding_left: i64,
|
|
padding_right: i64,
|
|
border_left: i64,
|
|
border_right: i64,
|
|
stretch: Option<i64>,
|
|
override_width: Option<i64>,
|
|
) -> Option<i64> {
|
|
let intrinsic_width = |size: &Size| {
|
|
matches!(
|
|
size,
|
|
Size::Content | Size::MinContent | Size::MaxContent | Size::Auto
|
|
)
|
|
};
|
|
if intrinsic_width(min_width) || intrinsic_width(max_width) {
|
|
return None;
|
|
}
|
|
let definite = |size| {
|
|
resolve_definite_width(
|
|
size,
|
|
context,
|
|
sizing,
|
|
padding_left,
|
|
padding_right,
|
|
border_left,
|
|
border_right,
|
|
stretch,
|
|
)
|
|
};
|
|
let minimum = definite(min_width).unwrap_or(0);
|
|
let maximum = definite(max_width);
|
|
let preferred = override_width
|
|
.or_else(|| definite(width))
|
|
.or_else(|| matches!(width, Size::Auto).then_some(stretch).flatten())
|
|
.or(maximum);
|
|
preferred.map(|preferred| minimum.max(preferred.max(0).min(maximum.unwrap_or(i64::MAX))))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_definite_box_content_height(
|
|
height: &Size,
|
|
min_height: &Size,
|
|
max_height: &Size,
|
|
context: LayoutContext,
|
|
sizing: BoxSizing,
|
|
padding_top: i64,
|
|
padding_bottom: i64,
|
|
override_height: Option<i64>,
|
|
) -> Option<i64> {
|
|
let minimum = resolve_height(
|
|
min_height,
|
|
Some(0),
|
|
context,
|
|
sizing,
|
|
padding_top,
|
|
padding_bottom,
|
|
)
|
|
.unwrap_or(0);
|
|
let maximum = resolve_height(
|
|
max_height,
|
|
None,
|
|
context,
|
|
sizing,
|
|
padding_top,
|
|
padding_bottom,
|
|
)
|
|
.unwrap_or(i64::MAX);
|
|
let preferred = override_height
|
|
.or_else(|| resolve_height(height, None, context, sizing, padding_top, padding_bottom))?;
|
|
Some(minimum.max(1).max(preferred.max(1).min(maximum)))
|
|
}
|
|
|
|
fn measured_max_width(text: &MeasuredText) -> i64 {
|
|
text.lines
|
|
.iter()
|
|
.map(|line| line.clusters.iter().map(|cluster| cluster.width).sum())
|
|
.max()
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn measured_min_width(text: &MeasuredText, wrap_mode: WrapMode) -> i64 {
|
|
if wrap_mode == WrapMode::None {
|
|
return measured_max_width(text);
|
|
}
|
|
text.lines
|
|
.iter()
|
|
.map(|line| {
|
|
let mut maximum = 0;
|
|
let mut run = 0;
|
|
for cluster in &line.clusters {
|
|
if cluster.space {
|
|
maximum = maximum.max(run);
|
|
run = 0;
|
|
} else if cluster.cjk {
|
|
maximum = maximum.max(run).max(cluster.width);
|
|
run = 0;
|
|
} else {
|
|
run += cluster.width;
|
|
}
|
|
}
|
|
maximum.max(run)
|
|
})
|
|
.max()
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn wrap_line(clusters: &[MeasuredCluster], max_width: i64, mode: WrapMode) -> Vec<Line> {
|
|
if clusters.is_empty() {
|
|
return vec![Line::default()];
|
|
}
|
|
let total: i64 = clusters.iter().map(|cluster| cluster.width).sum();
|
|
if mode == WrapMode::None || total <= max_width {
|
|
return vec![Line::from_clusters(clusters)];
|
|
}
|
|
|
|
let count = clusters.len();
|
|
let mut ranges = Vec::new();
|
|
let mut line_start = 0;
|
|
let mut current_width = 0;
|
|
let mut index = 0;
|
|
let flush_before = |end: usize,
|
|
ranges: &mut Vec<(usize, usize)>,
|
|
line_start: &mut usize,
|
|
current_width: &mut i64| {
|
|
if *line_start < end {
|
|
ranges.push((*line_start, end));
|
|
}
|
|
*line_start = end;
|
|
*current_width = 0;
|
|
};
|
|
|
|
while index < count {
|
|
let cluster_width = clusters[index].width;
|
|
if clusters[index].cjk {
|
|
if current_width + cluster_width > max_width {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
}
|
|
current_width += cluster_width;
|
|
index += 1;
|
|
} else if mode == WrapMode::Word {
|
|
let mut word_end = index;
|
|
let mut word_width = 0;
|
|
while word_end < count && !clusters[word_end].space && !clusters[word_end].cjk {
|
|
word_width += clusters[word_end].width;
|
|
word_end += 1;
|
|
}
|
|
if word_width > max_width {
|
|
while index < word_end {
|
|
let width = clusters[index].width;
|
|
if current_width + width > max_width {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
}
|
|
current_width += width;
|
|
index += 1;
|
|
}
|
|
} else if current_width + word_width <= max_width {
|
|
current_width += word_width;
|
|
index = word_end;
|
|
} else {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
current_width = word_width;
|
|
index = word_end;
|
|
}
|
|
if index < count && clusters[index].space {
|
|
let width = clusters[index].width;
|
|
if current_width + width <= max_width {
|
|
current_width += width;
|
|
index += 1;
|
|
} else {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
index += 1;
|
|
line_start = index;
|
|
}
|
|
}
|
|
} else {
|
|
if current_width + cluster_width > max_width {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
}
|
|
current_width += cluster_width;
|
|
index += 1;
|
|
}
|
|
}
|
|
if line_start < count {
|
|
ranges.push((line_start, count));
|
|
}
|
|
ranges
|
|
.into_iter()
|
|
.map(|(start, end)| Line::from_clusters(&clusters[start..end]))
|
|
.collect()
|
|
}
|
|
|
|
fn measured_lines(text: &MeasuredText, width: i64, mode: WrapMode) -> Vec<Line> {
|
|
text.lines
|
|
.iter()
|
|
.flat_map(|line| wrap_line(&line.clusters, width, mode))
|
|
.collect()
|
|
}
|
|
|
|
fn wrap_rendered_line(line: &Line, max_width: i64, mode: WrapMode) -> Vec<Line> {
|
|
if line.atoms.is_empty() {
|
|
return vec![Line::default()];
|
|
}
|
|
if mode == WrapMode::None || line.width <= max_width {
|
|
return vec![line.clone()];
|
|
}
|
|
|
|
let atoms = &line.atoms;
|
|
let count = atoms.len();
|
|
let mut ranges = Vec::new();
|
|
let mut line_start = 0;
|
|
let mut current_width = 0;
|
|
let mut index = 0;
|
|
let flush_before = |end: usize,
|
|
ranges: &mut Vec<(usize, usize)>,
|
|
line_start: &mut usize,
|
|
current_width: &mut i64| {
|
|
if *line_start < end {
|
|
ranges.push((*line_start, end));
|
|
}
|
|
*line_start = end;
|
|
*current_width = 0;
|
|
};
|
|
|
|
while index < count {
|
|
let atom_width = atoms[index].width();
|
|
if atoms[index].wrap_cjk() {
|
|
if current_width + atom_width > max_width {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
}
|
|
current_width += atom_width;
|
|
index += 1;
|
|
} else if mode == WrapMode::Word {
|
|
let mut word_end = index;
|
|
let mut word_width = 0;
|
|
while word_end < count && !atoms[word_end].wrap_space() && !atoms[word_end].wrap_cjk() {
|
|
word_width += atoms[word_end].width();
|
|
word_end += 1;
|
|
}
|
|
if word_width > max_width {
|
|
while index < word_end {
|
|
let width = atoms[index].width();
|
|
if current_width + width > max_width {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
}
|
|
current_width += width;
|
|
index += 1;
|
|
}
|
|
} else if current_width + word_width <= max_width {
|
|
current_width += word_width;
|
|
index = word_end;
|
|
} else {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
current_width = word_width;
|
|
index = word_end;
|
|
}
|
|
if index < count && atoms[index].wrap_space() {
|
|
let width = atoms[index].width();
|
|
if current_width + width <= max_width {
|
|
current_width += width;
|
|
index += 1;
|
|
} else {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
index += 1;
|
|
line_start = index;
|
|
}
|
|
}
|
|
} else {
|
|
if current_width + atom_width > max_width {
|
|
flush_before(index, &mut ranges, &mut line_start, &mut current_width);
|
|
}
|
|
current_width += atom_width;
|
|
index += 1;
|
|
}
|
|
}
|
|
if line_start < count {
|
|
ranges.push((line_start, count));
|
|
}
|
|
ranges
|
|
.into_iter()
|
|
.map(|(start, end)| Line::from_atoms(&atoms[start..end]))
|
|
.collect()
|
|
}
|
|
|
|
fn wrap_rendered(rendered: Rendered, max_width: i64, mode: WrapMode) -> Rendered {
|
|
if mode == WrapMode::None {
|
|
return rendered;
|
|
}
|
|
let original_breaks = rendered.breaks;
|
|
let mut lines = Vec::new();
|
|
let mut breaks = Vec::new();
|
|
for (line_index, line) in rendered.lines.into_iter().enumerate() {
|
|
for (piece_index, piece) in wrap_rendered_line(&line, max_width, mode)
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
if !lines.is_empty() {
|
|
breaks.push(if piece_index == 0 {
|
|
original_breaks[line_index - 1].clone()
|
|
} else {
|
|
AtomProperties::default()
|
|
});
|
|
}
|
|
lines.push(piece);
|
|
}
|
|
}
|
|
Rendered { lines, breaks }
|
|
}
|
|
|
|
fn vertical_align(
|
|
mut lines: Vec<Line>,
|
|
height: usize,
|
|
align: VerticalAlign,
|
|
width: i64,
|
|
) -> Vec<Line> {
|
|
lines.truncate(height);
|
|
if lines.len() >= height {
|
|
return lines;
|
|
}
|
|
let rest = height - lines.len();
|
|
let top = match align {
|
|
VerticalAlign::Top => 0,
|
|
VerticalAlign::Bottom => rest,
|
|
VerticalAlign::Center => rest / 2,
|
|
};
|
|
let mut output = Vec::with_capacity(height);
|
|
output.extend((0..top).map(|_| Line::blank(width)));
|
|
output.append(&mut lines);
|
|
output.extend((0..(rest - top)).map(|_| Line::blank(width)));
|
|
output
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum FlexAxis {
|
|
Row,
|
|
Column,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum FlexMode {
|
|
Grow,
|
|
Shrink,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct FlexRuntimeItem<'a> {
|
|
source: &'a LayoutNode,
|
|
grow: f64,
|
|
shrink: f64,
|
|
align_self: FlexAlign,
|
|
min_main: i64,
|
|
max_main: Option<i64>,
|
|
base: i64,
|
|
hypothetical: i64,
|
|
target: i64,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct FlexSizedEntry {
|
|
rendered: Rendered,
|
|
cross: i64,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct FlexCrossLayout {
|
|
crosses: Vec<i64>,
|
|
leading: i64,
|
|
between: i64,
|
|
}
|
|
|
|
fn flex_axis(direction: FlexDirection) -> FlexAxis {
|
|
match direction {
|
|
FlexDirection::Row | FlexDirection::RowReverse => FlexAxis::Row,
|
|
FlexDirection::Column | FlexDirection::ColumnReverse => FlexAxis::Column,
|
|
}
|
|
}
|
|
|
|
fn flex_direction_reversed(direction: FlexDirection) -> bool {
|
|
matches!(
|
|
direction,
|
|
FlexDirection::RowReverse | FlexDirection::ColumnReverse
|
|
)
|
|
}
|
|
|
|
fn flex_horizontal_size(size: &Size, context: LayoutContext) -> Option<i64> {
|
|
match size {
|
|
Size::Auto | Size::Stretch | Size::Contain => context
|
|
.viewport_width_known
|
|
.then_some(context.viewport_width.max(0)),
|
|
Size::Pixels { value } | Size::Lines { value } => Some(*value),
|
|
Size::Viewport => context
|
|
.viewport_width_known
|
|
.then_some(context.viewport_width.max(0)),
|
|
Size::None | Size::Content => None,
|
|
Size::MinContent | Size::MaxContent | Size::FitContent { .. } => context
|
|
.viewport_width_known
|
|
.then_some(context.viewport_width.max(0)),
|
|
Size::ViewportHeight | Size::Add { .. } | Size::Subtract { .. } => None,
|
|
}
|
|
}
|
|
|
|
fn flex_vertical_size(size: &Size, context: LayoutContext) -> Option<i64> {
|
|
match size {
|
|
Size::Lines { .. }
|
|
| Size::Pixels { .. }
|
|
| Size::ViewportHeight
|
|
| Size::Add { .. }
|
|
| Size::Subtract { .. } => resolve_raw_height(size, context).map(|value| value.max(0)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn flex_inline_viewport(
|
|
axis: FlexAxis,
|
|
main_size: Option<i64>,
|
|
cross_size: Option<i64>,
|
|
) -> Option<i64> {
|
|
match axis {
|
|
FlexAxis::Row => main_size,
|
|
FlexAxis::Column => cross_size,
|
|
}
|
|
}
|
|
|
|
fn box_horizontal_side(node: &LayoutNode) -> Option<i64> {
|
|
if let LayoutNode::Box {
|
|
padding_left,
|
|
padding_right,
|
|
margin_left,
|
|
margin_right,
|
|
border_left,
|
|
border_right,
|
|
..
|
|
} = node
|
|
{
|
|
Some(padding_left + padding_right + margin_left + margin_right + border_left + border_right)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn box_vertical_side(node: &LayoutNode) -> Option<i64> {
|
|
if let LayoutNode::Box {
|
|
padding_top,
|
|
padding_bottom,
|
|
margin_top,
|
|
margin_bottom,
|
|
..
|
|
} = node
|
|
{
|
|
Some(padding_top + padding_bottom + margin_top + margin_bottom)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn box_content_intrinsics(
|
|
node: &LayoutNode,
|
|
context: LayoutContext,
|
|
) -> Result<Option<(i64, i64)>, String> {
|
|
let LayoutNode::Box {
|
|
content,
|
|
child,
|
|
content_min_width,
|
|
wrap_mode,
|
|
..
|
|
} = node
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
if let Some(text) = content {
|
|
Ok(Some((
|
|
content_min_width.unwrap_or_else(|| measured_min_width(text, *wrap_mode)),
|
|
measured_max_width(text),
|
|
)))
|
|
} else if let Some(child) = child {
|
|
// Ebox measures a composite box's child under the flex container's
|
|
// current inline viewport. Treating that width as unknown makes
|
|
// responsive descendants collapse to their narrow intrinsic form and
|
|
// produces a different automatic minimum from the visible renderer.
|
|
let rendered = render_node(child, context, true)?;
|
|
Ok(Some((
|
|
content_min_width.unwrap_or_else(|| rendered.min_content_width(*wrap_mode)),
|
|
rendered.max_width(),
|
|
)))
|
|
} else {
|
|
Ok(Some((0, 0)))
|
|
}
|
|
}
|
|
|
|
fn flex_box_resolve_width(
|
|
node: &LayoutNode,
|
|
size: &Size,
|
|
fallback: Option<i64>,
|
|
context: LayoutContext,
|
|
) -> Result<Option<i64>, String> {
|
|
let LayoutNode::Box {
|
|
box_sizing,
|
|
padding_left,
|
|
padding_right,
|
|
border_left,
|
|
border_right,
|
|
..
|
|
} = node
|
|
else {
|
|
return Ok(fallback);
|
|
};
|
|
let (min_content, max_content) = box_content_intrinsics(node, context)?.unwrap_or((0, 0));
|
|
let side = box_horizontal_side(node).unwrap_or(0);
|
|
let stretch = context
|
|
.viewport_width_known
|
|
.then_some((context.viewport_width - side).max(0));
|
|
Ok(resolve_width(
|
|
size,
|
|
fallback,
|
|
context,
|
|
*box_sizing,
|
|
*padding_left,
|
|
*padding_right,
|
|
*border_left,
|
|
*border_right,
|
|
stretch,
|
|
min_content,
|
|
max_content,
|
|
))
|
|
}
|
|
|
|
fn flex_box_resolve_height(
|
|
node: &LayoutNode,
|
|
size: &Size,
|
|
fallback: Option<i64>,
|
|
context: LayoutContext,
|
|
) -> Option<i64> {
|
|
let LayoutNode::Box {
|
|
box_sizing,
|
|
padding_top,
|
|
padding_bottom,
|
|
..
|
|
} = node
|
|
else {
|
|
return fallback;
|
|
};
|
|
resolve_height(
|
|
size,
|
|
fallback,
|
|
context,
|
|
*box_sizing,
|
|
*padding_top,
|
|
*padding_bottom,
|
|
)
|
|
}
|
|
|
|
fn flex_min_main(
|
|
source: &LayoutNode,
|
|
rendered: &Rendered,
|
|
axis: FlexAxis,
|
|
context: LayoutContext,
|
|
) -> Result<i64, String> {
|
|
let LayoutNode::Box {
|
|
min_width,
|
|
min_height,
|
|
wrap_mode,
|
|
content_min_width,
|
|
..
|
|
} = source
|
|
else {
|
|
return Ok(match axis {
|
|
FlexAxis::Row => rendered.max_width(),
|
|
FlexAxis::Column => rendered.height(),
|
|
});
|
|
};
|
|
Ok(match axis {
|
|
FlexAxis::Row => {
|
|
let side = box_horizontal_side(source).unwrap_or(0);
|
|
let declared =
|
|
flex_box_resolve_width(source, min_width, Some(0), context)?.unwrap_or(0);
|
|
if *wrap_mode == WrapMode::None {
|
|
rendered.max_width().max(side + declared)
|
|
} else {
|
|
let content_min = match content_min_width {
|
|
Some(content_min_width) => *content_min_width,
|
|
None => box_content_intrinsics(source, context)?.unwrap_or((0, 0)).0,
|
|
};
|
|
side + declared.max(content_min)
|
|
}
|
|
}
|
|
FlexAxis::Column => {
|
|
box_vertical_side(source).unwrap_or(0)
|
|
+ flex_box_resolve_height(source, min_height, Some(0), context)
|
|
.unwrap_or(0)
|
|
.max(1)
|
|
}
|
|
})
|
|
}
|
|
|
|
fn flex_max_main(
|
|
source: &LayoutNode,
|
|
axis: FlexAxis,
|
|
context: LayoutContext,
|
|
) -> Result<Option<i64>, String> {
|
|
let LayoutNode::Box {
|
|
max_width,
|
|
max_height,
|
|
..
|
|
} = source
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
Ok(match axis {
|
|
FlexAxis::Row => flex_box_resolve_width(source, max_width, None, context)?
|
|
.map(|value| value + box_horizontal_side(source).unwrap_or(0)),
|
|
FlexAxis::Column => flex_box_resolve_height(source, max_height, None, context)
|
|
.map(|value| value + box_vertical_side(source).unwrap_or(0)),
|
|
})
|
|
}
|
|
|
|
fn flex_horizontal_basis_value(size: &Size, context: LayoutContext) -> i64 {
|
|
match size {
|
|
Size::Pixels { value } | Size::Lines { value } => *value,
|
|
Size::Viewport => context.viewport_width.max(0),
|
|
Size::Stretch | Size::Contain => context.viewport_width.max(0),
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
fn flex_vertical_basis_value(size: &Size) -> i64 {
|
|
match size {
|
|
Size::Pixels { value } | Size::Lines { value } => *value,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
fn flex_basis_main(
|
|
source: &LayoutNode,
|
|
rendered: &Rendered,
|
|
axis: FlexAxis,
|
|
basis: &Size,
|
|
context: LayoutContext,
|
|
) -> Result<i64, String> {
|
|
let rendered_main = match axis {
|
|
FlexAxis::Row => rendered.max_width(),
|
|
FlexAxis::Column => rendered.height(),
|
|
};
|
|
if matches!(basis, Size::Auto) {
|
|
return Ok(rendered_main);
|
|
}
|
|
if matches!(basis, Size::Content) {
|
|
if axis == FlexAxis::Row && matches!(source, LayoutNode::Box { .. }) {
|
|
let (_, content_max) = box_content_intrinsics(source, context)?.unwrap_or((0, 0));
|
|
return Ok(box_horizontal_side(source).unwrap_or(0) + content_max);
|
|
}
|
|
return Ok(rendered_main);
|
|
}
|
|
if axis == FlexAxis::Row && matches!(source, LayoutNode::Box { .. }) {
|
|
let (_, content_max) = box_content_intrinsics(source, context)?.unwrap_or((0, 0));
|
|
let content = flex_box_resolve_width(source, basis, Some(content_max), context)?
|
|
.unwrap_or(content_max);
|
|
return Ok(box_horizontal_side(source).unwrap_or(0) + content);
|
|
}
|
|
Ok(match axis {
|
|
FlexAxis::Row => flex_horizontal_basis_value(basis, context),
|
|
FlexAxis::Column => flex_vertical_basis_value(basis),
|
|
})
|
|
}
|
|
|
|
fn flex_clamp_main(value: i64, minimum: i64, maximum: Option<i64>) -> i64 {
|
|
minimum.max(value.max(0).min(maximum.unwrap_or(999_999_999)))
|
|
}
|
|
|
|
// Plain boxes receive their flex target through BoxOverride. Composite
|
|
// layouts, including the exact wrapper emitted for a visual flex container,
|
|
// must instead resolve viewport sizes against the item's inline allocation.
|
|
fn flex_item_uses_inline_viewport(node: &LayoutNode) -> bool {
|
|
match node {
|
|
LayoutNode::Box {
|
|
content,
|
|
child: Some(child),
|
|
content_width_exact: true,
|
|
..
|
|
} if content.is_none() => matches!(child.as_ref(), LayoutNode::Flex { .. }),
|
|
LayoutNode::Box { .. } => false,
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
fn measure_flex_item<'a>(
|
|
item: &'a FlexItem,
|
|
axis: FlexAxis,
|
|
inline_viewport: Option<i64>,
|
|
context: LayoutContext,
|
|
) -> Result<FlexRuntimeItem<'a>, String> {
|
|
let uses_inline_viewport = flex_item_uses_inline_viewport(&item.node);
|
|
let measurement_context = LayoutContext {
|
|
viewport_width: if uses_inline_viewport {
|
|
inline_viewport.unwrap_or(0)
|
|
} else {
|
|
0
|
|
},
|
|
viewport_width_known: uses_inline_viewport && inline_viewport.is_some(),
|
|
viewport_height: context.viewport_height,
|
|
};
|
|
let rendered = render_node(&item.node, measurement_context, uses_inline_viewport)?;
|
|
let min_main = flex_min_main(&item.node, &rendered, axis, context)?;
|
|
let max_main = flex_max_main(&item.node, axis, context)?;
|
|
let base = flex_basis_main(&item.node, &rendered, axis, &item.basis, context)?.max(0);
|
|
let hypothetical = flex_clamp_main(base, min_main, max_main);
|
|
Ok(FlexRuntimeItem {
|
|
source: &item.node,
|
|
grow: item.grow,
|
|
shrink: item.shrink,
|
|
align_self: item.align_self,
|
|
min_main,
|
|
max_main,
|
|
base,
|
|
hypothetical,
|
|
target: hypothetical,
|
|
})
|
|
}
|
|
|
|
fn flex_break_lines<'a>(
|
|
items: Vec<FlexRuntimeItem<'a>>,
|
|
main_limit: Option<i64>,
|
|
main_gap: i64,
|
|
wrap: FlexWrap,
|
|
) -> Vec<Vec<FlexRuntimeItem<'a>>> {
|
|
if wrap == FlexWrap::Nowrap || main_limit.is_none() {
|
|
return vec![items];
|
|
}
|
|
let limit = main_limit.unwrap_or(0);
|
|
let mut lines = Vec::new();
|
|
let mut current = Vec::new();
|
|
let mut current_size = 0;
|
|
for item in items {
|
|
let next_size =
|
|
current_size + if current.is_empty() { 0 } else { main_gap } + item.hypothetical;
|
|
if !current.is_empty() && next_size > limit {
|
|
lines.push(current);
|
|
current = vec![item];
|
|
current_size = current[0].hypothetical;
|
|
} else {
|
|
current_size = next_size;
|
|
current.push(item);
|
|
}
|
|
}
|
|
if !current.is_empty() {
|
|
lines.push(current);
|
|
}
|
|
lines
|
|
}
|
|
|
|
fn flex_factor(item: &FlexRuntimeItem<'_>, mode: FlexMode) -> f64 {
|
|
match mode {
|
|
FlexMode::Grow => item.grow,
|
|
FlexMode::Shrink => item.shrink,
|
|
}
|
|
}
|
|
|
|
fn flex_distribution_weight(item: &FlexRuntimeItem<'_>, mode: FlexMode) -> f64 {
|
|
match mode {
|
|
FlexMode::Grow => item.grow,
|
|
FlexMode::Shrink => item.base as f64 * item.shrink,
|
|
}
|
|
}
|
|
|
|
fn flex_line_free_space(
|
|
line: &[FlexRuntimeItem<'_>],
|
|
targets: &[i64],
|
|
frozen: &[bool],
|
|
available: i64,
|
|
) -> i64 {
|
|
available
|
|
- line
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, item)| {
|
|
if frozen[index] {
|
|
targets[index]
|
|
} else {
|
|
item.base
|
|
}
|
|
})
|
|
.sum::<i64>()
|
|
}
|
|
|
|
fn flex_effective_free_space(initial: i64, free: i64, factor_total: f64) -> f64 {
|
|
if factor_total > 0.0 && factor_total < 1.0 {
|
|
let partial = initial as f64 * factor_total;
|
|
if partial.abs() < (free as f64).abs() {
|
|
partial
|
|
} else {
|
|
free as f64
|
|
}
|
|
} else {
|
|
free as f64
|
|
}
|
|
}
|
|
|
|
fn flex_distribute(amount: f64, weights: &[f64]) -> Vec<i64> {
|
|
let amount = amount.max(0.0).floor() as i64;
|
|
let total = weights
|
|
.iter()
|
|
.copied()
|
|
.filter(|weight| *weight > 0.0)
|
|
.sum::<f64>();
|
|
if amount <= 0 || total <= 0.0 {
|
|
return vec![0; weights.len()];
|
|
}
|
|
let mut shares = weights
|
|
.iter()
|
|
.map(|weight| {
|
|
if *weight > 0.0 {
|
|
((amount as f64 * *weight) / total).floor() as i64
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut remaining = amount - shares.iter().sum::<i64>();
|
|
while remaining > 0 {
|
|
for (share, weight) in shares.iter_mut().zip(weights) {
|
|
if remaining == 0 {
|
|
break;
|
|
}
|
|
if *weight > 0.0 {
|
|
*share += 1;
|
|
remaining -= 1;
|
|
}
|
|
}
|
|
}
|
|
shares
|
|
}
|
|
|
|
fn flex_size_line(line: &mut [FlexRuntimeItem<'_>], main_limit: Option<i64>, main_gap: i64) {
|
|
let count = line.len();
|
|
let Some(limit) = main_limit else {
|
|
return;
|
|
};
|
|
let available = limit - main_gap * count.saturating_sub(1) as i64;
|
|
let hypothetical_total = line.iter().map(|item| item.hypothetical).sum::<i64>();
|
|
let mode = if hypothetical_total < available {
|
|
FlexMode::Grow
|
|
} else {
|
|
FlexMode::Shrink
|
|
};
|
|
let mut targets = line.iter().map(|item| item.base).collect::<Vec<_>>();
|
|
let mut frozen = vec![false; count];
|
|
for (index, item) in line.iter().enumerate() {
|
|
let factor = flex_factor(item, mode);
|
|
if factor <= 0.0
|
|
|| (mode == FlexMode::Grow && item.base > item.hypothetical)
|
|
|| (mode == FlexMode::Shrink && item.base < item.hypothetical)
|
|
{
|
|
targets[index] = item.hypothetical;
|
|
frozen[index] = true;
|
|
}
|
|
}
|
|
let initial_free = flex_line_free_space(line, &targets, &frozen, available);
|
|
loop {
|
|
let free = flex_line_free_space(line, &targets, &frozen, available);
|
|
let active = (0..count)
|
|
.filter(|index| !frozen[*index])
|
|
.collect::<Vec<_>>();
|
|
let weights = active
|
|
.iter()
|
|
.map(|index| flex_distribution_weight(&line[*index], mode))
|
|
.collect::<Vec<_>>();
|
|
let weight_total = weights.iter().sum::<f64>();
|
|
let factor_total = active
|
|
.iter()
|
|
.map(|index| flex_factor(&line[*index], mode))
|
|
.sum::<f64>();
|
|
let effective = flex_effective_free_space(initial_free, free, factor_total);
|
|
if active.is_empty()
|
|
|| weight_total <= 0.0
|
|
|| (mode == FlexMode::Grow && effective < 0.0)
|
|
|| (mode == FlexMode::Shrink && effective > 0.0)
|
|
{
|
|
break;
|
|
}
|
|
let deltas = flex_distribute(effective.abs(), &weights);
|
|
let mut min_violations = Vec::new();
|
|
let mut max_violations = Vec::new();
|
|
let mut total_violation = 0;
|
|
for ((index, delta), _) in active.iter().zip(deltas).zip(&weights) {
|
|
let item = &line[*index];
|
|
let candidate = match mode {
|
|
FlexMode::Grow => item.base + delta,
|
|
FlexMode::Shrink => item.base - delta,
|
|
};
|
|
let clamped = flex_clamp_main(candidate, item.min_main, item.max_main);
|
|
let adjustment = clamped - candidate;
|
|
targets[*index] = clamped;
|
|
if adjustment > 0 {
|
|
min_violations.push(*index);
|
|
} else if adjustment < 0 {
|
|
max_violations.push(*index);
|
|
}
|
|
total_violation += adjustment;
|
|
}
|
|
let to_freeze = if total_violation == 0 {
|
|
active
|
|
} else if total_violation > 0 {
|
|
if min_violations.is_empty() {
|
|
active
|
|
} else {
|
|
min_violations
|
|
}
|
|
} else if max_violations.is_empty() {
|
|
active
|
|
} else {
|
|
max_violations
|
|
};
|
|
for index in to_freeze {
|
|
frozen[index] = true;
|
|
}
|
|
}
|
|
for (item, target) in line.iter_mut().zip(targets) {
|
|
item.target = target;
|
|
}
|
|
}
|
|
|
|
fn flex_line_main_size(line: &[FlexRuntimeItem<'_>], main_gap: i64) -> i64 {
|
|
line.iter().map(|item| item.target).sum::<i64>()
|
|
+ main_gap * line.len().saturating_sub(1) as i64
|
|
}
|
|
|
|
fn flex_spacing(mode: FlexAlign, leftover: i64, count: usize, base_gap: i64) -> (i64, i64, i64) {
|
|
let leftover = leftover.max(0);
|
|
match mode {
|
|
FlexAlign::FlexEnd | FlexAlign::End | FlexAlign::Right | FlexAlign::Bottom => {
|
|
(leftover, base_gap, 0)
|
|
}
|
|
FlexAlign::Center => {
|
|
let leading = leftover / 2;
|
|
(leading, base_gap, leftover - leading)
|
|
}
|
|
FlexAlign::SpaceBetween if count > 1 => (0, base_gap + leftover / (count as i64 - 1), 0),
|
|
FlexAlign::SpaceBetween => (0, base_gap, leftover),
|
|
FlexAlign::SpaceAround => {
|
|
let unit = if count > 0 {
|
|
leftover / count as i64
|
|
} else {
|
|
0
|
|
};
|
|
let leading = unit / 2;
|
|
(
|
|
leading,
|
|
base_gap + unit,
|
|
leftover - leading - unit * count.saturating_sub(1) as i64,
|
|
)
|
|
}
|
|
FlexAlign::SpaceEvenly => {
|
|
let unit = if count > 0 {
|
|
leftover / (count as i64 + 1)
|
|
} else {
|
|
0
|
|
};
|
|
(unit, base_gap + unit, unit)
|
|
}
|
|
_ => (0, base_gap, leftover),
|
|
}
|
|
}
|
|
|
|
fn flex_cross_offset(align: FlexAlign, extra: i64) -> i64 {
|
|
match align {
|
|
FlexAlign::FlexEnd
|
|
| FlexAlign::End
|
|
| FlexAlign::SelfEnd
|
|
| FlexAlign::Right
|
|
| FlexAlign::Bottom => extra,
|
|
FlexAlign::Center => extra / 2,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
fn flex_resolved_align(item: &FlexRuntimeItem<'_>, container: FlexAlign) -> FlexAlign {
|
|
if item.align_self == FlexAlign::Auto {
|
|
container
|
|
} else {
|
|
item.align_self
|
|
}
|
|
}
|
|
|
|
fn pad_rendered_width(mut rendered: Rendered, width: i64, align: FlexAlign) -> Rendered {
|
|
for line in &mut rendered.lines {
|
|
let extra = (width - line.width).max(0);
|
|
let left = match align {
|
|
FlexAlign::FlexEnd | FlexAlign::End | FlexAlign::Right | FlexAlign::SelfEnd => extra,
|
|
FlexAlign::Center => extra / 2,
|
|
_ => 0,
|
|
};
|
|
line.prepend_space(left);
|
|
line.push_space(extra - left);
|
|
}
|
|
rendered
|
|
}
|
|
|
|
fn pad_rendered_height(rendered: Rendered, height: i64, offset: i64, width: i64) -> Rendered {
|
|
let extra = (height - rendered.height()).max(0);
|
|
let top = extra.min(offset.max(0));
|
|
let bottom = extra - top;
|
|
let mut parts = Vec::with_capacity(3);
|
|
if top > 0 {
|
|
parts.push(Rendered::from_lines(
|
|
(0..top).map(|_| Line::blank(width)).collect(),
|
|
));
|
|
}
|
|
parts.push(rendered);
|
|
if bottom > 0 {
|
|
parts.push(Rendered::from_lines(
|
|
(0..bottom).map(|_| Line::blank(width)).collect(),
|
|
));
|
|
}
|
|
stack_vertical(parts)
|
|
}
|
|
|
|
fn box_override_for_flex(
|
|
source: &LayoutNode,
|
|
axis: FlexAxis,
|
|
main: i64,
|
|
cross: Option<i64>,
|
|
stretch: bool,
|
|
) -> Option<BoxOverride> {
|
|
if !matches!(source, LayoutNode::Box { .. }) {
|
|
return None;
|
|
}
|
|
let horizontal_side = box_horizontal_side(source).unwrap_or(0);
|
|
let vertical_side = box_vertical_side(source).unwrap_or(0);
|
|
Some(match axis {
|
|
FlexAxis::Row => BoxOverride {
|
|
content_width: Some((main - horizontal_side).max(0)),
|
|
content_height: (stretch && cross.is_some())
|
|
.then_some((cross.unwrap_or(0) - vertical_side).max(0)),
|
|
declared_width: None,
|
|
},
|
|
FlexAxis::Column => BoxOverride {
|
|
content_width: (stretch && cross.is_some())
|
|
.then_some((cross.unwrap_or(0) - horizontal_side).max(0)),
|
|
content_height: Some((main - vertical_side).max(0)),
|
|
declared_width: None,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn render_flex_sized_entry(
|
|
item: &FlexRuntimeItem<'_>,
|
|
axis: FlexAxis,
|
|
main: i64,
|
|
cross: Option<i64>,
|
|
container_align: FlexAlign,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
) -> Result<FlexSizedEntry, String> {
|
|
let align = flex_resolved_align(item, container_align);
|
|
let stretch = matches!(align, FlexAlign::Stretch | FlexAlign::Normal);
|
|
let source_box = !flex_item_uses_inline_viewport(item.source);
|
|
let item_viewport = flex_inline_viewport(axis, Some(main), cross);
|
|
let render_context = LayoutContext {
|
|
viewport_width: if source_box {
|
|
context.viewport_width
|
|
} else {
|
|
item_viewport.unwrap_or(context.viewport_width)
|
|
},
|
|
viewport_width_known: if source_box {
|
|
context.viewport_width_known
|
|
} else {
|
|
item_viewport.is_some() || context.viewport_width_known
|
|
},
|
|
viewport_height: context.viewport_height,
|
|
};
|
|
let override_size = box_override_for_flex(item.source, axis, main, cross, stretch);
|
|
let mut rendered =
|
|
render_node_with_override(item.source, render_context, intrinsic, override_size)?;
|
|
match axis {
|
|
FlexAxis::Row => {
|
|
rendered = pad_rendered_width(rendered, main, FlexAlign::FlexStart);
|
|
}
|
|
FlexAxis::Column => {
|
|
let width = rendered.max_width();
|
|
rendered = pad_rendered_height(rendered, main, 0, width);
|
|
}
|
|
}
|
|
if let Some(cross) = cross {
|
|
match axis {
|
|
FlexAxis::Row => {
|
|
let width = main.max(rendered.max_width());
|
|
let extra = (cross - rendered.height()).max(0);
|
|
rendered =
|
|
pad_rendered_height(rendered, cross, flex_cross_offset(align, extra), width);
|
|
}
|
|
FlexAxis::Column => {
|
|
rendered = pad_rendered_width(rendered, cross, align);
|
|
}
|
|
}
|
|
}
|
|
let rendered_cross = cross.unwrap_or_else(|| match axis {
|
|
FlexAxis::Row => rendered.height(),
|
|
FlexAxis::Column => rendered.max_width(),
|
|
});
|
|
Ok(FlexSizedEntry {
|
|
rendered,
|
|
cross: rendered_cross,
|
|
})
|
|
}
|
|
|
|
fn concat_horizontal_sized(parts: Vec<(Rendered, i64)>, target_height: i64) -> Rendered {
|
|
let height = parts
|
|
.iter()
|
|
.map(|(rendered, _)| rendered.height())
|
|
.max()
|
|
.unwrap_or(1)
|
|
.max(target_height);
|
|
let mut lines = Vec::with_capacity(height as usize);
|
|
for index in 0..height as usize {
|
|
let mut line = Line::default();
|
|
for (rendered, width) in &parts {
|
|
if let Some(part) = rendered.lines.get(index) {
|
|
line.append(part);
|
|
} else {
|
|
line.push_space(*width);
|
|
}
|
|
}
|
|
lines.push(line);
|
|
}
|
|
Rendered::from_lines(lines)
|
|
}
|
|
|
|
fn stack_vertical(parts: Vec<Rendered>) -> Rendered {
|
|
let mut lines = Vec::new();
|
|
let mut breaks = Vec::new();
|
|
for mut part in parts {
|
|
if part.lines.is_empty() {
|
|
continue;
|
|
}
|
|
if !lines.is_empty() {
|
|
breaks.push(AtomProperties::default());
|
|
}
|
|
lines.append(&mut part.lines);
|
|
breaks.append(&mut part.breaks);
|
|
}
|
|
if lines.is_empty() {
|
|
lines.push(Line::default());
|
|
}
|
|
Rendered { lines, breaks }
|
|
}
|
|
|
|
fn slice_rendered(rendered: Rendered, start: i64, height: i64) -> Rendered {
|
|
let start = usize::try_from(start.max(0)).unwrap_or(usize::MAX);
|
|
let height = usize::try_from(height.max(0)).unwrap_or(0);
|
|
let end = start.saturating_add(height).min(rendered.lines.len());
|
|
if start >= end {
|
|
return Rendered {
|
|
lines: Vec::new(),
|
|
breaks: Vec::new(),
|
|
};
|
|
}
|
|
let line_count = end - start;
|
|
let lines = rendered
|
|
.lines
|
|
.into_iter()
|
|
.skip(start)
|
|
.take(line_count)
|
|
.collect::<Vec<_>>();
|
|
let breaks = rendered
|
|
.breaks
|
|
.into_iter()
|
|
.skip(start)
|
|
.take(line_count.saturating_sub(1))
|
|
.collect::<Vec<_>>();
|
|
Rendered { lines, breaks }
|
|
}
|
|
|
|
fn exact_rendered_height(node: &LayoutNode, context: LayoutContext) -> Option<i64> {
|
|
match node {
|
|
LayoutNode::Box {
|
|
height,
|
|
min_height,
|
|
max_height,
|
|
box_sizing,
|
|
padding_top,
|
|
padding_bottom,
|
|
margin_top,
|
|
margin_bottom,
|
|
overflow,
|
|
..
|
|
} => {
|
|
if *overflow == Overflow::Visible {
|
|
return None;
|
|
}
|
|
let content_height = resolve_definite_box_content_height(
|
|
height,
|
|
min_height,
|
|
max_height,
|
|
context,
|
|
*box_sizing,
|
|
*padding_top,
|
|
*padding_bottom,
|
|
None,
|
|
)?;
|
|
Some(content_height + padding_top + padding_bottom + margin_top + margin_bottom)
|
|
}
|
|
LayoutNode::Row { children } => children
|
|
.iter()
|
|
.map(|child| exact_rendered_height(child, context))
|
|
.try_fold(1_i64, |maximum, height| {
|
|
height.map(|height| maximum.max(height))
|
|
}),
|
|
LayoutNode::Column { children } => children
|
|
.iter()
|
|
.map(|child| exact_rendered_height(child, context))
|
|
.try_fold(0_i64, |total, height| {
|
|
height.and_then(|height| total.checked_add(height))
|
|
}),
|
|
LayoutNode::Flex { .. } => None,
|
|
}
|
|
}
|
|
|
|
fn render_node_window(
|
|
node: &LayoutNode,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
start: i64,
|
|
height: i64,
|
|
) -> Option<Result<Rendered, String>> {
|
|
let total_height = exact_rendered_height(node, context)?;
|
|
if start <= 0 && height >= total_height {
|
|
return Some(render_node(node, context, intrinsic));
|
|
}
|
|
match node {
|
|
LayoutNode::Column { children } if !intrinsic && context.viewport_width_known => {
|
|
Some(render_column_window(children, context, start, height))
|
|
}
|
|
_ => Some(
|
|
render_node(node, context, intrinsic)
|
|
.map(|rendered| slice_rendered(rendered, start, height)),
|
|
),
|
|
}
|
|
}
|
|
|
|
fn render_column_window(
|
|
children: &[LayoutNode],
|
|
context: LayoutContext,
|
|
start: i64,
|
|
height: i64,
|
|
) -> Result<Rendered, String> {
|
|
let mut leaves = Vec::new();
|
|
collect_column_leaves(children, &mut leaves);
|
|
let end = start.saturating_add(height).max(start);
|
|
let target = context.viewport_width.max(0);
|
|
let mut offset = 0_i64;
|
|
let mut parts = Vec::new();
|
|
|
|
for child in leaves {
|
|
let child_height = exact_rendered_height(child, context)
|
|
.ok_or_else(|| "Native layout column window has an unbounded child".to_owned())?;
|
|
let child_end = offset.saturating_add(child_height);
|
|
if child_end <= start {
|
|
offset = child_end;
|
|
continue;
|
|
}
|
|
if offset >= end {
|
|
break;
|
|
}
|
|
|
|
let child_start = start.saturating_sub(offset);
|
|
let child_window_height = (child_end.min(end) - (offset + child_start)).max(0);
|
|
let mut rendered = if child_start == 0 && child_window_height >= child_height {
|
|
render_node(child, context, false)?
|
|
} else {
|
|
render_node_window(child, context, false, child_start, child_window_height)
|
|
.unwrap_or_else(|| {
|
|
render_node(child, context, false)
|
|
.map(|rendered| slice_rendered(rendered, child_start, child_window_height))
|
|
})?
|
|
};
|
|
|
|
let extra = (target - rendered.first_width()).max(0);
|
|
for line in &mut rendered.lines {
|
|
line.push_space(extra);
|
|
}
|
|
if extra > 0 {
|
|
rendered.breaks =
|
|
vec![AtomProperties::default(); rendered.lines.len().saturating_sub(1)];
|
|
}
|
|
if !rendered.lines.is_empty() {
|
|
parts.push(rendered);
|
|
}
|
|
offset = child_end;
|
|
}
|
|
|
|
Ok(stack_vertical(parts))
|
|
}
|
|
|
|
fn flex_line_cross(
|
|
line: &[FlexRuntimeItem<'_>],
|
|
axis: FlexAxis,
|
|
container_align: FlexAlign,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
) -> Result<i64, String> {
|
|
let mut maximum = 1;
|
|
for item in line {
|
|
maximum = maximum.max(
|
|
render_flex_sized_entry(
|
|
item,
|
|
axis,
|
|
item.target,
|
|
None,
|
|
container_align,
|
|
context,
|
|
intrinsic,
|
|
)?
|
|
.cross,
|
|
);
|
|
}
|
|
Ok(maximum)
|
|
}
|
|
|
|
fn flex_layout_cross(
|
|
crosses: Vec<i64>,
|
|
container_cross: Option<i64>,
|
|
cross_gap: i64,
|
|
align: FlexAlign,
|
|
single_line: bool,
|
|
) -> FlexCrossLayout {
|
|
let count = crosses.len();
|
|
let natural = crosses.iter().sum::<i64>() + cross_gap * count.saturating_sub(1) as i64;
|
|
let leftover = container_cross.map_or(0, |cross| cross - natural);
|
|
if let Some(cross) = container_cross {
|
|
if single_line && count == 1 {
|
|
return FlexCrossLayout {
|
|
crosses: vec![cross],
|
|
leading: 0,
|
|
between: cross_gap,
|
|
};
|
|
}
|
|
if leftover > 0 && matches!(align, FlexAlign::Stretch | FlexAlign::Normal) {
|
|
let extras = flex_distribute(leftover as f64, &vec![1.0; count]);
|
|
return FlexCrossLayout {
|
|
crosses: crosses
|
|
.into_iter()
|
|
.zip(extras)
|
|
.map(|(value, extra)| value + extra)
|
|
.collect(),
|
|
leading: 0,
|
|
between: cross_gap,
|
|
};
|
|
}
|
|
}
|
|
let (leading, between, _) = flex_spacing(align, leftover, count, cross_gap);
|
|
FlexCrossLayout {
|
|
crosses,
|
|
leading,
|
|
between,
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn render_flex_row_line(
|
|
line: &[FlexRuntimeItem<'_>],
|
|
line_cross: i64,
|
|
main_size: i64,
|
|
main_gap: i64,
|
|
justify: FlexAlign,
|
|
align: FlexAlign,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
) -> Result<Rendered, String> {
|
|
let line_main = flex_line_main_size(line, main_gap);
|
|
let (leading, between, trailing) =
|
|
flex_spacing(justify, main_size - line_main, line.len(), main_gap);
|
|
let mut parts = vec![(Rendered::from_lines(vec![Line::blank(leading)]), leading)];
|
|
for (index, item) in line.iter().enumerate() {
|
|
let entry = render_flex_sized_entry(
|
|
item,
|
|
FlexAxis::Row,
|
|
item.target,
|
|
Some(line_cross),
|
|
align,
|
|
context,
|
|
intrinsic,
|
|
)?;
|
|
parts.push((entry.rendered, item.target));
|
|
if index + 1 < line.len() {
|
|
parts.push((Rendered::from_lines(vec![Line::blank(between)]), between));
|
|
}
|
|
}
|
|
if trailing > 0 {
|
|
parts.push((Rendered::from_lines(vec![Line::blank(trailing)]), trailing));
|
|
}
|
|
Ok(concat_horizontal_sized(parts, line_cross))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn render_flex_column_line(
|
|
line: &[FlexRuntimeItem<'_>],
|
|
line_cross: i64,
|
|
main_size: Option<i64>,
|
|
main_gap: i64,
|
|
justify: FlexAlign,
|
|
align: FlexAlign,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
) -> Result<Rendered, String> {
|
|
let line_main = flex_line_main_size(line, main_gap);
|
|
let target_main = main_size.unwrap_or(line_main);
|
|
let (leading, between, _) =
|
|
flex_spacing(justify, target_main - line_main, line.len(), main_gap);
|
|
let mut parts = Vec::new();
|
|
if leading > 0 {
|
|
parts.push(Rendered::from_lines(
|
|
(0..leading).map(|_| Line::blank(line_cross)).collect(),
|
|
));
|
|
}
|
|
for (index, item) in line.iter().enumerate() {
|
|
parts.push(
|
|
render_flex_sized_entry(
|
|
item,
|
|
FlexAxis::Column,
|
|
item.target,
|
|
Some(line_cross),
|
|
align,
|
|
context,
|
|
intrinsic,
|
|
)?
|
|
.rendered,
|
|
);
|
|
if index + 1 < line.len() && between > 0 {
|
|
parts.push(Rendered::from_lines(
|
|
(0..between).map(|_| Line::blank(line_cross)).collect(),
|
|
));
|
|
}
|
|
}
|
|
let rendered = stack_vertical(parts);
|
|
Ok(pad_rendered_height(rendered, target_main, 0, line_cross))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn render_flex_row(
|
|
mut lines: Vec<Vec<FlexRuntimeItem<'_>>>,
|
|
main_size: Option<i64>,
|
|
cross_size: Option<i64>,
|
|
main_gap: i64,
|
|
cross_gap: i64,
|
|
justify: FlexAlign,
|
|
align: FlexAlign,
|
|
align_content: FlexAlign,
|
|
single_line: bool,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
) -> Result<Rendered, String> {
|
|
for line in &mut lines {
|
|
flex_size_line(line, main_size, main_gap);
|
|
}
|
|
let container_main = main_size.unwrap_or_else(|| {
|
|
lines
|
|
.iter()
|
|
.map(|line| flex_line_main_size(line, main_gap))
|
|
.max()
|
|
.unwrap_or(0)
|
|
});
|
|
let crosses = if cross_size.is_some() && single_line {
|
|
vec![cross_size.unwrap_or(0)]
|
|
} else {
|
|
lines
|
|
.iter()
|
|
.map(|line| flex_line_cross(line, FlexAxis::Row, align, context, intrinsic))
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
};
|
|
let cross_layout =
|
|
flex_layout_cross(crosses, cross_size, cross_gap, align_content, single_line);
|
|
let mut parts = Vec::new();
|
|
if cross_layout.leading > 0 {
|
|
parts.push(Rendered::from_lines(
|
|
(0..cross_layout.leading)
|
|
.map(|_| Line::blank(container_main))
|
|
.collect(),
|
|
));
|
|
}
|
|
let line_count = lines.len();
|
|
for (index, (line, line_cross)) in lines
|
|
.iter()
|
|
.zip(cross_layout.crosses.iter().copied())
|
|
.enumerate()
|
|
{
|
|
parts.push(render_flex_row_line(
|
|
line,
|
|
line_cross,
|
|
container_main,
|
|
main_gap,
|
|
justify,
|
|
align,
|
|
context,
|
|
intrinsic,
|
|
)?);
|
|
if index + 1 < line_count && cross_layout.between > 0 {
|
|
parts.push(Rendered::from_lines(
|
|
(0..cross_layout.between)
|
|
.map(|_| Line::blank(container_main))
|
|
.collect(),
|
|
));
|
|
}
|
|
}
|
|
let rendered = stack_vertical(parts);
|
|
Ok(if let Some(cross) = cross_size {
|
|
pad_rendered_height(rendered, cross, 0, container_main)
|
|
} else {
|
|
rendered
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn render_flex_column(
|
|
mut lines: Vec<Vec<FlexRuntimeItem<'_>>>,
|
|
main_size: Option<i64>,
|
|
cross_size: Option<i64>,
|
|
main_gap: i64,
|
|
cross_gap: i64,
|
|
justify: FlexAlign,
|
|
align: FlexAlign,
|
|
align_content: FlexAlign,
|
|
single_line: bool,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
) -> Result<Rendered, String> {
|
|
for line in &mut lines {
|
|
flex_size_line(line, main_size, main_gap);
|
|
}
|
|
let crosses = if cross_size.is_some() && single_line {
|
|
vec![cross_size.unwrap_or(0)]
|
|
} else {
|
|
lines
|
|
.iter()
|
|
.map(|line| flex_line_cross(line, FlexAxis::Column, align, context, intrinsic))
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
};
|
|
let cross_layout =
|
|
flex_layout_cross(crosses, cross_size, cross_gap, align_content, single_line);
|
|
let mut parts = vec![(
|
|
Rendered::from_lines(vec![Line::blank(cross_layout.leading)]),
|
|
cross_layout.leading,
|
|
)];
|
|
let line_count = lines.len();
|
|
for (index, (line, line_cross)) in lines
|
|
.iter()
|
|
.zip(cross_layout.crosses.iter().copied())
|
|
.enumerate()
|
|
{
|
|
let rendered = render_flex_column_line(
|
|
line, line_cross, main_size, main_gap, justify, align, context, intrinsic,
|
|
)?;
|
|
parts.push((rendered, line_cross));
|
|
if index + 1 < line_count {
|
|
parts.push((
|
|
Rendered::from_lines(vec![Line::blank(cross_layout.between)]),
|
|
cross_layout.between,
|
|
));
|
|
}
|
|
}
|
|
let mut rendered = concat_horizontal_sized(parts, 1);
|
|
if let Some(cross) = cross_size {
|
|
rendered = pad_rendered_width(rendered, cross, FlexAlign::FlexStart);
|
|
}
|
|
Ok(rendered)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn render_flex(
|
|
direction: FlexDirection,
|
|
wrap: FlexWrap,
|
|
justify: FlexAlign,
|
|
align_items: FlexAlign,
|
|
align_content: FlexAlign,
|
|
width: &Size,
|
|
height: &Size,
|
|
row_gap: i64,
|
|
column_gap: i64,
|
|
source_items: &[FlexItem],
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
) -> Result<Rendered, String> {
|
|
let axis = flex_axis(direction);
|
|
let width = flex_horizontal_size(width, context);
|
|
let height = flex_vertical_size(height, context);
|
|
let (main_size, cross_size) = match axis {
|
|
FlexAxis::Row => (width, height),
|
|
FlexAxis::Column => (height, width),
|
|
};
|
|
let inline_viewport = flex_inline_viewport(axis, main_size, cross_size);
|
|
let mut indices = (0..source_items.len()).collect::<Vec<_>>();
|
|
indices.sort_by_key(|index| (source_items[*index].order, *index));
|
|
let mut items = indices
|
|
.into_iter()
|
|
.map(|index| measure_flex_item(&source_items[index], axis, inline_viewport, context))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
if flex_direction_reversed(direction) {
|
|
items.reverse();
|
|
}
|
|
let (main_gap, cross_gap) = match axis {
|
|
FlexAxis::Row => (column_gap, row_gap),
|
|
FlexAxis::Column => (row_gap, column_gap),
|
|
};
|
|
let mut lines = flex_break_lines(items, main_size, main_gap, wrap);
|
|
if wrap == FlexWrap::WrapReverse {
|
|
lines.reverse();
|
|
}
|
|
let single_line = wrap == FlexWrap::Nowrap;
|
|
match axis {
|
|
FlexAxis::Row => render_flex_row(
|
|
lines,
|
|
main_size,
|
|
cross_size,
|
|
main_gap,
|
|
cross_gap,
|
|
justify,
|
|
align_items,
|
|
align_content,
|
|
single_line,
|
|
context,
|
|
intrinsic,
|
|
),
|
|
FlexAxis::Column => render_flex_column(
|
|
lines,
|
|
main_size,
|
|
cross_size,
|
|
main_gap,
|
|
cross_gap,
|
|
justify,
|
|
align_items,
|
|
align_content,
|
|
single_line,
|
|
context,
|
|
intrinsic,
|
|
),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
struct BoxOverride {
|
|
content_width: Option<i64>,
|
|
content_height: Option<i64>,
|
|
declared_width: Option<i64>,
|
|
}
|
|
|
|
fn render_node(
|
|
node: &LayoutNode,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
) -> Result<Rendered, String> {
|
|
render_node_with_override(node, context, intrinsic, None)
|
|
}
|
|
|
|
fn render_node_with_override(
|
|
node: &LayoutNode,
|
|
context: LayoutContext,
|
|
intrinsic: bool,
|
|
size_override: Option<BoxOverride>,
|
|
) -> Result<Rendered, String> {
|
|
#[cfg(test)]
|
|
TEST_RENDER_NODE_COUNT.with(|count| {
|
|
if let Some(value) = count.get() {
|
|
count.set(Some(value + 1));
|
|
}
|
|
});
|
|
match node {
|
|
LayoutNode::Box {
|
|
region_id,
|
|
content,
|
|
child,
|
|
content_width_exact,
|
|
content_min_width: _,
|
|
width,
|
|
min_width,
|
|
max_width,
|
|
height,
|
|
min_height,
|
|
max_height,
|
|
box_sizing,
|
|
padding_left,
|
|
padding_right,
|
|
padding_top,
|
|
padding_bottom,
|
|
margin_left,
|
|
margin_right,
|
|
margin_top,
|
|
margin_bottom,
|
|
border_left,
|
|
border_right,
|
|
foreground_style,
|
|
background_style,
|
|
border_left_style,
|
|
border_right_style,
|
|
border_top_style,
|
|
border_bottom_style,
|
|
surface_template_id,
|
|
text_align,
|
|
vertical_align: vertical,
|
|
overflow,
|
|
wrap_mode,
|
|
scroll_offset,
|
|
} => {
|
|
let declared_width_override =
|
|
size_override.and_then(|override_size| override_size.declared_width);
|
|
let declared_width_size = declared_width_override.map(|value| Size::Pixels { value });
|
|
let effective_width = declared_width_size.as_ref().unwrap_or(width);
|
|
let side_width = padding_left
|
|
+ padding_right
|
|
+ margin_left
|
|
+ margin_right
|
|
+ border_left
|
|
+ border_right;
|
|
let stretch = context
|
|
.viewport_width_known
|
|
.then_some((context.viewport_width - side_width).max(0));
|
|
|
|
let preliminary_child_width = size_override
|
|
.and_then(|override_size| override_size.content_width)
|
|
.or_else(|| {
|
|
resolve_child_viewport_width(
|
|
effective_width,
|
|
min_width,
|
|
max_width,
|
|
context,
|
|
*box_sizing,
|
|
*padding_left,
|
|
*padding_right,
|
|
*border_left,
|
|
*border_right,
|
|
stretch,
|
|
)
|
|
});
|
|
let preliminary_child_height = size_override
|
|
.and_then(|override_size| override_size.content_height)
|
|
.or_else(|| {
|
|
resolve_height(
|
|
height,
|
|
None,
|
|
context,
|
|
*box_sizing,
|
|
*padding_top,
|
|
*padding_bottom,
|
|
)
|
|
});
|
|
let intrinsic_child = matches!(effective_width, Size::MaxContent);
|
|
let definite_content_width = resolve_definite_box_content_width(
|
|
effective_width,
|
|
min_width,
|
|
max_width,
|
|
context,
|
|
*box_sizing,
|
|
*padding_left,
|
|
*padding_right,
|
|
*border_left,
|
|
*border_right,
|
|
stretch,
|
|
size_override.and_then(|override_size| override_size.content_width),
|
|
);
|
|
let definite_content_height = resolve_definite_box_content_height(
|
|
height,
|
|
min_height,
|
|
max_height,
|
|
context,
|
|
*box_sizing,
|
|
*padding_top,
|
|
*padding_bottom,
|
|
size_override.and_then(|override_size| override_size.content_height),
|
|
);
|
|
let window_rendering_disabled = {
|
|
#[cfg(test)]
|
|
{
|
|
TEST_DISABLE_WINDOW_RENDER.with(Cell::get)
|
|
}
|
|
#[cfg(not(test))]
|
|
{
|
|
false
|
|
}
|
|
};
|
|
let simple_scroll_window = *overflow == Overflow::Scroll
|
|
&& child.is_some()
|
|
&& definite_content_width.is_some()
|
|
&& definite_content_height.is_some()
|
|
&& !intrinsic
|
|
&& !intrinsic_child
|
|
&& !window_rendering_disabled
|
|
&& *padding_left == 0
|
|
&& *padding_right == 0
|
|
&& *padding_top == 0
|
|
&& *padding_bottom == 0
|
|
&& *margin_left == 0
|
|
&& *margin_right == 0
|
|
&& *margin_top == 0
|
|
&& *margin_bottom == 0
|
|
&& *border_left == 0
|
|
&& *border_right == 0
|
|
&& border_top_style.is_none()
|
|
&& border_bottom_style.is_none()
|
|
&& *vertical == VerticalAlign::Top;
|
|
let mut windowed_child_start = None;
|
|
let mut windowed_child_height = None;
|
|
let mut child_rendered = if let Some(child) = child {
|
|
let child_context = LayoutContext {
|
|
viewport_width: if intrinsic_child {
|
|
0
|
|
} else {
|
|
preliminary_child_width.unwrap_or(context.viewport_width)
|
|
},
|
|
viewport_width_known: !intrinsic_child
|
|
&& (preliminary_child_width.is_some() || context.viewport_width_known),
|
|
viewport_height: preliminary_child_height.unwrap_or(context.viewport_height),
|
|
};
|
|
if simple_scroll_window {
|
|
if let Some(total_height) = exact_rendered_height(child, child_context) {
|
|
let content_height = definite_content_height.expect("checked above");
|
|
let max_offset = (total_height - content_height).max(0);
|
|
let start = (*scroll_offset).max(0).min(max_offset);
|
|
windowed_child_start = Some(start);
|
|
windowed_child_height = Some(total_height);
|
|
Some(
|
|
render_node_window(
|
|
child,
|
|
child_context,
|
|
intrinsic || intrinsic_child,
|
|
start,
|
|
content_height,
|
|
)
|
|
.expect("exact height checked above")?,
|
|
)
|
|
} else {
|
|
Some(render_node(
|
|
child,
|
|
child_context,
|
|
intrinsic || intrinsic_child,
|
|
)?)
|
|
}
|
|
} else {
|
|
Some(render_node(
|
|
child,
|
|
child_context,
|
|
intrinsic || intrinsic_child,
|
|
)?)
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let (min_content, max_content) = if let Some(text) = content {
|
|
(
|
|
measured_min_width(text, *wrap_mode),
|
|
measured_max_width(text),
|
|
)
|
|
} else {
|
|
let rendered = child_rendered.as_ref().expect("validated child");
|
|
(rendered.min_content_width(*wrap_mode), rendered.max_width())
|
|
};
|
|
let auto_width = stretch.unwrap_or(max_content);
|
|
let minimum = resolve_width(
|
|
min_width,
|
|
Some(0),
|
|
context,
|
|
*box_sizing,
|
|
*padding_left,
|
|
*padding_right,
|
|
*border_left,
|
|
*border_right,
|
|
stretch,
|
|
min_content,
|
|
max_content,
|
|
)
|
|
.unwrap_or(0);
|
|
let maximum = resolve_width(
|
|
max_width,
|
|
None,
|
|
context,
|
|
*box_sizing,
|
|
*padding_left,
|
|
*padding_right,
|
|
*border_left,
|
|
*border_right,
|
|
stretch,
|
|
min_content,
|
|
max_content,
|
|
)
|
|
.unwrap_or(i64::MAX);
|
|
let preferred = size_override
|
|
.and_then(|override_size| override_size.content_width)
|
|
.unwrap_or_else(|| {
|
|
resolve_width(
|
|
effective_width,
|
|
Some(auto_width),
|
|
context,
|
|
*box_sizing,
|
|
*padding_left,
|
|
*padding_right,
|
|
*border_left,
|
|
*border_right,
|
|
stretch,
|
|
min_content,
|
|
max_content,
|
|
)
|
|
.unwrap_or(auto_width)
|
|
});
|
|
let content_width = minimum.max(preferred.max(0).min(maximum));
|
|
let transparent_preformatted = child.is_some()
|
|
&& *content_width_exact
|
|
&& *wrap_mode == WrapMode::None
|
|
&& *text_align == HorizontalAlign::Left
|
|
&& *vertical == VerticalAlign::Top
|
|
&& matches!(height, Size::Auto)
|
|
&& matches!(min_height, Size::Lines { value: 0 })
|
|
&& matches!(max_height, Size::None)
|
|
&& *overflow == Overflow::Scroll
|
|
&& *padding_left == 0
|
|
&& *padding_right == 0
|
|
&& *padding_top == 0
|
|
&& *padding_bottom == 0
|
|
&& *margin_left == 0
|
|
&& *margin_right == 0
|
|
&& *margin_top == 0
|
|
&& *margin_bottom == 0
|
|
&& *border_left == 0
|
|
&& *border_right == 0
|
|
&& foreground_style.is_none()
|
|
&& background_style.is_none()
|
|
&& border_left_style.is_none()
|
|
&& border_right_style.is_none()
|
|
&& border_top_style.is_none()
|
|
&& border_bottom_style.is_none()
|
|
&& child_rendered.as_ref().is_some_and(|rendered| {
|
|
rendered
|
|
.lines
|
|
.iter()
|
|
.all(|line| !line.atoms.is_empty() && line.width == content_width)
|
|
});
|
|
if transparent_preformatted {
|
|
let mut rendered = child_rendered.take().expect("validated child");
|
|
for (index, line) in rendered.lines.iter_mut().enumerate() {
|
|
line.own_content(*region_id, index as i64);
|
|
line.apply_property_template(*surface_template_id);
|
|
}
|
|
return Ok(rendered);
|
|
}
|
|
|
|
let mut formatted = if let Some(text) = content {
|
|
measured_lines(text, content_width, *wrap_mode)
|
|
.into_iter()
|
|
.map(|line| line.padded(content_width, *text_align))
|
|
.collect::<Vec<_>>()
|
|
} else {
|
|
let rendered = child_rendered.expect("validated child");
|
|
let uniform_width = rendered
|
|
.lines
|
|
.iter()
|
|
.all(|line| line.width == content_width);
|
|
let preserve_exact_width = *content_width_exact && *wrap_mode == WrapMode::None;
|
|
let rendered = if *wrap_mode != WrapMode::None && !uniform_width {
|
|
wrap_rendered(rendered, content_width, *wrap_mode)
|
|
} else {
|
|
rendered
|
|
};
|
|
rendered
|
|
.lines
|
|
.into_iter()
|
|
.map(|line| {
|
|
if preserve_exact_width {
|
|
line
|
|
} else {
|
|
line.padded(content_width, *text_align)
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let text_height = windowed_child_height.unwrap_or(formatted.len() as i64);
|
|
let minimum_height = resolve_height(
|
|
min_height,
|
|
Some(0),
|
|
context,
|
|
*box_sizing,
|
|
*padding_top,
|
|
*padding_bottom,
|
|
)
|
|
.unwrap_or(0);
|
|
let maximum_height = resolve_height(
|
|
max_height,
|
|
None,
|
|
context,
|
|
*box_sizing,
|
|
*padding_top,
|
|
*padding_bottom,
|
|
)
|
|
.unwrap_or(i64::MAX);
|
|
let preferred_height = size_override
|
|
.and_then(|override_size| override_size.content_height)
|
|
.unwrap_or_else(|| {
|
|
resolve_height(
|
|
height,
|
|
None,
|
|
context,
|
|
*box_sizing,
|
|
*padding_top,
|
|
*padding_bottom,
|
|
)
|
|
.unwrap_or(text_height)
|
|
});
|
|
let content_height = minimum_height
|
|
.max(1)
|
|
.max(preferred_height.max(1).min(maximum_height));
|
|
let simple_scroll_rendered = *overflow == Overflow::Scroll
|
|
&& text_height >= content_height
|
|
&& *padding_left == 0
|
|
&& *padding_right == 0
|
|
&& *padding_top == 0
|
|
&& *padding_bottom == 0
|
|
&& *margin_left == 0
|
|
&& *margin_right == 0
|
|
&& *margin_top == 0
|
|
&& *margin_bottom == 0
|
|
&& *border_left == 0
|
|
&& *border_right == 0
|
|
&& border_top_style.is_none()
|
|
&& border_bottom_style.is_none()
|
|
&& *vertical == VerticalAlign::Top;
|
|
|
|
let mut content_index_start = 0_i64;
|
|
let mut overflow_lines = Vec::new();
|
|
if let Some(start) = windowed_child_start {
|
|
content_index_start = start;
|
|
} else if formatted.len() > content_height as usize {
|
|
match overflow {
|
|
Overflow::Scroll => {
|
|
let max_offset = formatted.len() - content_height as usize;
|
|
let start = (*scroll_offset as usize).min(max_offset);
|
|
if simple_scroll_rendered {
|
|
content_index_start = start as i64;
|
|
}
|
|
formatted = formatted[start..start + content_height as usize].to_vec();
|
|
}
|
|
Overflow::Hidden => {
|
|
formatted.truncate(content_height as usize);
|
|
}
|
|
Overflow::Visible => {
|
|
overflow_lines = formatted.split_off(content_height as usize);
|
|
}
|
|
}
|
|
}
|
|
for (index, line) in formatted.iter_mut().enumerate() {
|
|
line.own_content(*region_id, content_index_start + index as i64);
|
|
}
|
|
let mut lines =
|
|
vertical_align(formatted, content_height as usize, *vertical, content_width)
|
|
.into_iter()
|
|
.map(|line| {
|
|
if simple_scroll_rendered {
|
|
line
|
|
} else {
|
|
line.collapse_whitespace_content(content_width, *region_id)
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut padded =
|
|
Vec::with_capacity(lines.len() + *padding_top as usize + *padding_bottom as usize);
|
|
padded.extend((0..*padding_top).map(|_| {
|
|
Line::blank_with_properties(
|
|
content_width,
|
|
region_properties(RegionRole::PaddingTop, *region_id, None),
|
|
)
|
|
}));
|
|
padded.append(&mut lines);
|
|
padded.extend((0..*padding_bottom).map(|_| {
|
|
Line::blank_with_properties(
|
|
content_width,
|
|
region_properties(RegionRole::PaddingBottom, *region_id, None),
|
|
)
|
|
}));
|
|
|
|
for line in &mut padded {
|
|
line.prepend_space_with_properties(
|
|
*padding_left,
|
|
region_properties(RegionRole::PaddingLeft, *region_id, None),
|
|
);
|
|
line.push_space_with_properties(
|
|
*padding_right,
|
|
region_properties(RegionRole::PaddingRight, *region_id, None),
|
|
);
|
|
line.apply_style(*foreground_style);
|
|
line.apply_style(*background_style);
|
|
line.prepend_space_with_properties(
|
|
*border_left,
|
|
region_properties(RegionRole::BorderLeft, *region_id, *border_left_style),
|
|
);
|
|
line.push_space_with_properties(
|
|
*border_right,
|
|
region_properties(RegionRole::BorderRight, *region_id, *border_right_style),
|
|
);
|
|
}
|
|
if border_top_style.is_some() {
|
|
if let Some(first) = padded.first_mut() {
|
|
first.apply_style(*border_top_style);
|
|
first.apply_role(RegionRole::BorderTop, *region_id);
|
|
}
|
|
}
|
|
if border_bottom_style.is_some() {
|
|
if let Some(last) = padded.last_mut() {
|
|
last.apply_style(*border_bottom_style);
|
|
last.apply_role(RegionRole::BorderBottom, *region_id);
|
|
}
|
|
}
|
|
for line in &mut padded {
|
|
line.apply_property_template(*surface_template_id);
|
|
}
|
|
for line in &mut padded {
|
|
line.prepend_space_with_properties(
|
|
*margin_left,
|
|
region_properties(RegionRole::MarginLeft, *region_id, None),
|
|
);
|
|
line.push_space_with_properties(
|
|
*margin_right,
|
|
region_properties(RegionRole::MarginRight, *region_id, None),
|
|
);
|
|
}
|
|
let total_width = content_width + side_width;
|
|
let mut output =
|
|
Vec::with_capacity(padded.len() + *margin_top as usize + *margin_bottom as usize);
|
|
output.extend((0..*margin_top).map(|_| {
|
|
Line::blank_with_properties(
|
|
total_width,
|
|
region_properties(RegionRole::MarginTop, *region_id, None),
|
|
)
|
|
}));
|
|
output.append(&mut padded);
|
|
output.extend((0..*margin_bottom).map(|_| {
|
|
Line::blank_with_properties(
|
|
total_width,
|
|
region_properties(RegionRole::MarginBottom, *region_id, None),
|
|
)
|
|
}));
|
|
let mut rendered = Rendered::from_lines(output);
|
|
if !overflow_lines.is_empty() {
|
|
let left_space = margin_left + border_left + padding_left;
|
|
let right_space = padding_right + border_right + margin_right;
|
|
for line in &mut overflow_lines {
|
|
line.apply_style(*foreground_style);
|
|
line.prepend_space(left_space);
|
|
line.push_space(right_space);
|
|
}
|
|
rendered = stack_vertical(vec![rendered, Rendered::from_lines(overflow_lines)]);
|
|
}
|
|
if *overflow == Overflow::Scroll && text_height > content_height {
|
|
rendered.apply_scroll_window(*region_id);
|
|
}
|
|
Ok(rendered)
|
|
}
|
|
LayoutNode::Row { children } => {
|
|
let rendered = children
|
|
.iter()
|
|
.map(|child| render_node(child, context, intrinsic))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let height = rendered
|
|
.iter()
|
|
.map(|item| item.lines.len())
|
|
.max()
|
|
.unwrap_or(1);
|
|
let mut lines = Vec::with_capacity(height);
|
|
for index in 0..height {
|
|
let mut line = Line::default();
|
|
for item in &rendered {
|
|
if let Some(child_line) = item.lines.get(index) {
|
|
line.append(child_line);
|
|
} else {
|
|
line.push_space(item.first_width());
|
|
}
|
|
}
|
|
lines.push(line);
|
|
}
|
|
Ok(Rendered::from_lines(lines))
|
|
}
|
|
LayoutNode::Column { children } => {
|
|
let mut leaves = Vec::new();
|
|
collect_column_leaves(children, &mut leaves);
|
|
let rendered = leaves
|
|
.into_iter()
|
|
.map(|child| render_node(child, context, intrinsic))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let maximum = rendered
|
|
.iter()
|
|
.map(Rendered::first_width)
|
|
.max()
|
|
.unwrap_or(0);
|
|
let target = if intrinsic || !context.viewport_width_known {
|
|
maximum
|
|
} else {
|
|
context.viewport_width.max(0)
|
|
};
|
|
let mut parts = Vec::with_capacity(rendered.len());
|
|
for mut item in rendered {
|
|
let extra = (target - item.first_width()).max(0);
|
|
for line in &mut item.lines {
|
|
line.push_space(extra);
|
|
}
|
|
if extra > 0 {
|
|
item.breaks =
|
|
vec![AtomProperties::default(); item.lines.len().saturating_sub(1)];
|
|
}
|
|
parts.push(item);
|
|
}
|
|
Ok(stack_vertical(parts))
|
|
}
|
|
LayoutNode::Flex {
|
|
direction,
|
|
wrap,
|
|
justify,
|
|
align_items,
|
|
align_content,
|
|
width,
|
|
height,
|
|
row_gap,
|
|
column_gap,
|
|
items,
|
|
} => render_flex(
|
|
*direction,
|
|
*wrap,
|
|
*justify,
|
|
*align_items,
|
|
*align_content,
|
|
width,
|
|
height,
|
|
*row_gap,
|
|
*column_gap,
|
|
items,
|
|
context,
|
|
intrinsic,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn collect_column_leaves<'a>(children: &'a [LayoutNode], output: &mut Vec<&'a LayoutNode>) {
|
|
for child in children {
|
|
if let LayoutNode::Column { children } = child {
|
|
collect_column_leaves(children, output);
|
|
} else {
|
|
output.push(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn counted_layout_tape(
|
|
document: &LayoutDocument,
|
|
context: LayoutContext,
|
|
) -> (LayoutTape, usize) {
|
|
TEST_RENDER_NODE_COUNT.with(|count| count.set(Some(0)));
|
|
let tape = document.layout_tape(context, None).unwrap();
|
|
let count = TEST_RENDER_NODE_COUNT.with(|count| count.replace(None).unwrap());
|
|
(tape, count)
|
|
}
|
|
|
|
fn eager_layout_tape(document: &LayoutDocument, context: LayoutContext) -> LayoutTape {
|
|
TEST_DISABLE_WINDOW_RENDER.with(|disabled| disabled.set(true));
|
|
let tape = document.layout_tape(context, None).unwrap();
|
|
TEST_DISABLE_WINDOW_RENDER.with(|disabled| disabled.set(false));
|
|
tape
|
|
}
|
|
|
|
fn document(json: &str) -> LayoutDocument {
|
|
serde_json::from_str(json).unwrap()
|
|
}
|
|
|
|
fn cluster(text: &str, width: i64, source_template_id: Option<u32>) -> MeasuredCluster {
|
|
MeasuredCluster {
|
|
text: text.to_owned(),
|
|
width,
|
|
cjk: false,
|
|
space: text == " ",
|
|
pixel_space: false,
|
|
source_template_id,
|
|
}
|
|
}
|
|
|
|
fn measured_text(lines: Vec<Vec<MeasuredCluster>>) -> MeasuredText {
|
|
MeasuredText {
|
|
lines: lines
|
|
.into_iter()
|
|
.map(|clusters| MeasuredLine { clusters })
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn text_box(
|
|
region_id: i64,
|
|
content: MeasuredText,
|
|
surface_template_id: Option<u32>,
|
|
) -> LayoutNode {
|
|
LayoutNode::Box {
|
|
region_id,
|
|
content: Some(Box::new(content)),
|
|
child: None,
|
|
content_width_exact: false,
|
|
content_min_width: None,
|
|
width: Size::Content,
|
|
min_width: Size::Pixels { value: 0 },
|
|
max_width: Size::None,
|
|
height: Size::Auto,
|
|
min_height: Size::Lines { value: 0 },
|
|
max_height: Size::None,
|
|
box_sizing: BoxSizing::BorderBox,
|
|
padding_left: 0,
|
|
padding_right: 0,
|
|
padding_top: 0,
|
|
padding_bottom: 0,
|
|
margin_left: 0,
|
|
margin_right: 0,
|
|
margin_top: 0,
|
|
margin_bottom: 0,
|
|
border_left: 0,
|
|
border_right: 0,
|
|
foreground_style: None,
|
|
background_style: None,
|
|
border_left_style: None,
|
|
border_right_style: None,
|
|
border_top_style: None,
|
|
border_bottom_style: None,
|
|
surface_template_id,
|
|
text_align: HorizontalAlign::Left,
|
|
vertical_align: VerticalAlign::Top,
|
|
overflow: Overflow::Scroll,
|
|
wrap_mode: WrapMode::None,
|
|
scroll_offset: 0,
|
|
}
|
|
}
|
|
|
|
fn child_box(
|
|
region_id: i64,
|
|
child: LayoutNode,
|
|
surface_template_id: Option<u32>,
|
|
) -> LayoutNode {
|
|
LayoutNode::Box {
|
|
region_id,
|
|
content: None,
|
|
child: Some(Box::new(child)),
|
|
content_width_exact: true,
|
|
content_min_width: None,
|
|
width: Size::Content,
|
|
min_width: Size::Pixels { value: 0 },
|
|
max_width: Size::None,
|
|
height: Size::Auto,
|
|
min_height: Size::Lines { value: 0 },
|
|
max_height: Size::None,
|
|
box_sizing: BoxSizing::BorderBox,
|
|
padding_left: 0,
|
|
padding_right: 0,
|
|
padding_top: 0,
|
|
padding_bottom: 0,
|
|
margin_left: 0,
|
|
margin_right: 0,
|
|
margin_top: 0,
|
|
margin_bottom: 0,
|
|
border_left: 0,
|
|
border_right: 0,
|
|
foreground_style: None,
|
|
background_style: None,
|
|
border_left_style: None,
|
|
border_right_style: None,
|
|
border_top_style: None,
|
|
border_bottom_style: None,
|
|
surface_template_id,
|
|
text_align: HorizontalAlign::Left,
|
|
vertical_align: VerticalAlign::Top,
|
|
overflow: Overflow::Scroll,
|
|
wrap_mode: WrapMode::None,
|
|
scroll_offset: 0,
|
|
}
|
|
}
|
|
|
|
fn fixed_scroll_column_document(scroll_offset: i64) -> LayoutDocument {
|
|
let children = (0..20)
|
|
.map(|index| {
|
|
let mut node = text_box(
|
|
index + 2,
|
|
measured_text(vec![vec![cluster(&format!("line-{index:03}"), 8, None)]]),
|
|
Some((index % 2) as u32),
|
|
);
|
|
let LayoutNode::Box { width, height, .. } = &mut node else {
|
|
unreachable!();
|
|
};
|
|
*width = Size::Viewport;
|
|
*height = Size::Lines { value: 1 };
|
|
node
|
|
})
|
|
.collect();
|
|
let mut root = child_box(1, LayoutNode::Column { children }, Some(2));
|
|
let LayoutNode::Box {
|
|
width,
|
|
height,
|
|
content_width_exact,
|
|
scroll_offset: offset,
|
|
..
|
|
} = &mut root
|
|
else {
|
|
unreachable!();
|
|
};
|
|
*width = Size::Viewport;
|
|
*height = Size::Lines { value: 5 };
|
|
*content_width_exact = false;
|
|
*offset = scroll_offset;
|
|
LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: 3,
|
|
styles: Vec::new(),
|
|
root,
|
|
}
|
|
}
|
|
|
|
fn test_context() -> LayoutContext {
|
|
LayoutContext {
|
|
viewport_width: 80,
|
|
viewport_width_known: true,
|
|
viewport_height: 24,
|
|
}
|
|
}
|
|
|
|
fn complete_identity() -> TapeIdentity {
|
|
TapeIdentity {
|
|
session_id: 1,
|
|
generation: 2,
|
|
key: 3,
|
|
runtime_revision: 4,
|
|
context_hash: 5,
|
|
viewport_width: 80,
|
|
viewport_height: 24,
|
|
root_width: 80,
|
|
complete: true,
|
|
}
|
|
}
|
|
|
|
fn literal_from_full_tape(tape: &[u8]) -> &str {
|
|
let literal_length = u64::from_le_bytes(tape[112..120].try_into().unwrap()) as usize;
|
|
let line_count = u32::from_le_bytes(tape[120..124].try_into().unwrap()) as usize;
|
|
let literal_start = 152 + line_count * 8;
|
|
std::str::from_utf8(&tape[literal_start..literal_start + literal_length]).unwrap()
|
|
}
|
|
|
|
fn metadata_from_full_tape(tape: &[u8]) -> &str {
|
|
let literal_length = u64::from_le_bytes(tape[112..120].try_into().unwrap()) as usize;
|
|
let line_count = u32::from_le_bytes(tape[120..124].try_into().unwrap()) as usize;
|
|
let metadata_length = u32::from_le_bytes(tape[124..128].try_into().unwrap()) as usize;
|
|
let literal_start = 152 + line_count * 8;
|
|
let metadata_start = literal_start + literal_length;
|
|
std::str::from_utf8(&tape[metadata_start..metadata_start + metadata_length]).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_layout_versions_and_negative_geometry() {
|
|
let invalid_version = document(
|
|
r#"{"version":3,"space-width":8,"style-count":0,"root":{"type":"row","children":[{"type":"column","children":[]}]}}"#,
|
|
);
|
|
assert!(invalid_version.validate().is_err());
|
|
|
|
let negative = document(
|
|
r#"{"version":2,"space-width":8,"style-count":0,"root":{"type":"box","region-id":1,"content":{"lines":[{"clusters":[{"text":"x","width":8,"cjk":false,"space":false}]}]},"child":null,"content-width-exact":false,"width":{"kind":"pixels","value":-1},"min-width":{"kind":"pixels","value":0},"max-width":{"kind":"none"},"height":{"kind":"auto"},"min-height":{"kind":"lines","value":0},"max-height":{"kind":"none"},"box-sizing":"border-box","padding-left":0,"padding-right":0,"padding-top":0,"padding-bottom":0,"margin-left":0,"margin-right":0,"margin-top":0,"margin-bottom":0,"border-left":0,"border-right":0,"foreground-style":null,"background-style":null,"border-left-style":null,"border-right-style":null,"border-top-style":null,"border-bottom-style":null,"text-align":"left","vertical-align":"top","overflow":"scroll","wrap-mode":"word","scroll-offset":0}}"#,
|
|
);
|
|
assert!(negative.validate().is_err());
|
|
assert!(validate_dimension("width", MAX_LAYOUT_DIMENSION + 1).is_err());
|
|
let mut work_units = MAX_LAYOUT_WORK_UNITS;
|
|
assert!(add_work_units(&mut work_units, 1).is_err());
|
|
let mut context_work = 0;
|
|
assert!(add_context_size_work(
|
|
&Size::ViewportHeight,
|
|
LayoutContext {
|
|
viewport_width: 80,
|
|
viewport_width_known: true,
|
|
viewport_height: MAX_LAYOUT_WORK_UNITS as i64 + 1,
|
|
},
|
|
&mut context_work,
|
|
)
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn word_and_character_wrapping_preserve_measured_clusters() {
|
|
let clusters = "alpha beta"
|
|
.chars()
|
|
.map(|character| MeasuredCluster {
|
|
text: character.to_string(),
|
|
width: 1,
|
|
cjk: false,
|
|
space: character == ' ',
|
|
pixel_space: false,
|
|
source_template_id: None,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let word_lines = wrap_line(&clusters, 5, WrapMode::Word);
|
|
let texts = word_lines
|
|
.iter()
|
|
.map(|line| {
|
|
line.atoms
|
|
.iter()
|
|
.map(|atom| match atom {
|
|
Atom::Text { text, .. } => text.as_str(),
|
|
Atom::Space { .. } => " ",
|
|
})
|
|
.collect::<String>()
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(texts, ["alpha", "beta"]);
|
|
|
|
let character_lines = wrap_line(&clusters, 3, WrapMode::Char);
|
|
assert_eq!(character_lines.len(), 4);
|
|
assert!(character_lines.iter().all(|line| line.width <= 3));
|
|
}
|
|
|
|
#[test]
|
|
fn source_template_ids_follow_measured_clusters() {
|
|
let root = text_box(
|
|
1,
|
|
measured_text(vec![vec![
|
|
cluster("a", 1, Some(0)),
|
|
cluster("b", 1, None),
|
|
cluster("c", 1, Some(1)),
|
|
]]),
|
|
None,
|
|
);
|
|
let document = LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: 2,
|
|
styles: Vec::new(),
|
|
root,
|
|
};
|
|
document.validate().unwrap();
|
|
let tape = document.layout_tape(test_context(), None).unwrap();
|
|
let flat = flatten_layout_tape(tape, true).unwrap();
|
|
|
|
assert_eq!(
|
|
flat.characters
|
|
.iter()
|
|
.map(|character| character.properties.property_template_ids.as_slice())
|
|
.collect::<Vec<_>>(),
|
|
vec![&[0][..], &[][..], &[1][..]]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn whitespace_only_source_template_survives_content_collapse() {
|
|
let mut root = text_box(1, measured_text(vec![vec![cluster(" ", 1, Some(0))]]), None);
|
|
let LayoutNode::Box { overflow, .. } = &mut root else {
|
|
unreachable!();
|
|
};
|
|
*overflow = Overflow::Hidden;
|
|
|
|
let document = LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: 1,
|
|
styles: Vec::new(),
|
|
root,
|
|
};
|
|
document.validate().unwrap();
|
|
let tape = document.layout_tape(test_context(), None).unwrap();
|
|
let flat = flatten_layout_tape(tape, true).unwrap();
|
|
|
|
assert_eq!(flat.characters.len(), 1);
|
|
assert_eq!(flat.characters[0].properties.property_template_ids, vec![0]);
|
|
}
|
|
|
|
#[test]
|
|
fn surface_template_ids_cover_border_box_but_not_margins_or_newlines() {
|
|
let mut root = text_box(
|
|
1,
|
|
measured_text(vec![
|
|
vec![cluster("x", 1, None)],
|
|
vec![cluster("y", 1, None)],
|
|
]),
|
|
Some(0),
|
|
);
|
|
let LayoutNode::Box {
|
|
padding_left,
|
|
padding_right,
|
|
padding_top,
|
|
padding_bottom,
|
|
margin_left,
|
|
margin_right,
|
|
margin_top,
|
|
margin_bottom,
|
|
..
|
|
} = &mut root
|
|
else {
|
|
unreachable!();
|
|
};
|
|
*padding_left = 1;
|
|
*padding_right = 1;
|
|
*padding_top = 1;
|
|
*padding_bottom = 1;
|
|
*margin_left = 1;
|
|
*margin_right = 1;
|
|
*margin_top = 1;
|
|
*margin_bottom = 1;
|
|
|
|
let document = LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: 1,
|
|
styles: Vec::new(),
|
|
root,
|
|
};
|
|
document.validate().unwrap();
|
|
let tape = document.layout_tape(test_context(), None).unwrap();
|
|
let flat = flatten_layout_tape(tape, true).unwrap();
|
|
let rows = flat
|
|
.characters
|
|
.split(|character| character.value == '\n')
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(rows.len(), 6);
|
|
assert!(rows[0]
|
|
.iter()
|
|
.all(|character| character.properties.property_template_ids.is_empty()));
|
|
assert!(rows[5]
|
|
.iter()
|
|
.all(|character| character.properties.property_template_ids.is_empty()));
|
|
for row in &rows[1..5] {
|
|
assert_eq!(row.len(), 5);
|
|
assert!(row[0].properties.property_template_ids.is_empty());
|
|
assert_eq!(row[1].properties.property_template_ids, vec![0]);
|
|
assert_eq!(row[2].properties.property_template_ids, vec![0]);
|
|
assert_eq!(row[3].properties.property_template_ids, vec![0]);
|
|
assert!(row[4].properties.property_template_ids.is_empty());
|
|
}
|
|
assert!(flat
|
|
.characters
|
|
.iter()
|
|
.filter(|character| character.value == '\n')
|
|
.all(|character| character.properties.property_template_ids.is_empty()));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_template_ids_are_ordered_inner_to_outer() {
|
|
let inner = text_box(
|
|
1,
|
|
measured_text(vec![vec![cluster("x", 1, Some(0))]]),
|
|
Some(1),
|
|
);
|
|
let outer = child_box(2, inner, Some(2));
|
|
let document = LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: 3,
|
|
styles: Vec::new(),
|
|
root: outer,
|
|
};
|
|
document.validate().unwrap();
|
|
let tape = document.layout_tape(test_context(), None).unwrap();
|
|
let flat = flatten_layout_tape(tape, true).unwrap();
|
|
|
|
assert_eq!(flat.characters.len(), 1);
|
|
assert_eq!(
|
|
flat.characters[0].properties.property_template_ids,
|
|
vec![0, 1, 2]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn property_template_ids_are_validated_against_the_declared_table() {
|
|
let source = LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: 1,
|
|
styles: Vec::new(),
|
|
root: text_box(1, measured_text(vec![vec![cluster("x", 1, Some(1))]]), None),
|
|
};
|
|
assert!(source.validate().is_err());
|
|
|
|
let surface = LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: 1,
|
|
styles: Vec::new(),
|
|
root: text_box(1, measured_text(vec![vec![cluster("x", 1, None)]]), Some(1)),
|
|
};
|
|
assert!(surface.validate().is_err());
|
|
|
|
let oversized_table = LayoutDocument {
|
|
property_template_count: MAX_PROPERTY_TEMPLATE_COUNT + 1,
|
|
..surface
|
|
};
|
|
assert!(oversized_table.validate().is_err());
|
|
|
|
let larger_than_atom_depth = LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: MAX_TAPE_PROPERTY_ENTRIES as u32 + 2,
|
|
styles: Vec::new(),
|
|
root: text_box(
|
|
1,
|
|
measured_text(vec![vec![cluster(
|
|
"x",
|
|
1,
|
|
Some(MAX_TAPE_PROPERTY_ENTRIES as u32 + 1),
|
|
)]]),
|
|
Some(MAX_TAPE_PROPERTY_ENTRIES as u32),
|
|
),
|
|
};
|
|
assert!(larger_than_atom_depth.validate().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn rendered_min_content_matches_elisp_separators_and_cjk_runs() {
|
|
let clusters = vec![
|
|
MeasuredCluster {
|
|
text: "a".to_owned(),
|
|
width: 2,
|
|
cjk: false,
|
|
space: false,
|
|
pixel_space: false,
|
|
source_template_id: None,
|
|
},
|
|
MeasuredCluster {
|
|
text: "b".to_owned(),
|
|
width: 3,
|
|
cjk: false,
|
|
space: false,
|
|
pixel_space: false,
|
|
source_template_id: None,
|
|
},
|
|
MeasuredCluster {
|
|
text: " ".to_owned(),
|
|
width: 4,
|
|
cjk: false,
|
|
space: true,
|
|
pixel_space: false,
|
|
source_template_id: None,
|
|
},
|
|
MeasuredCluster {
|
|
text: "界".to_owned(),
|
|
width: 9,
|
|
cjk: true,
|
|
space: false,
|
|
pixel_space: false,
|
|
source_template_id: None,
|
|
},
|
|
MeasuredCluster {
|
|
text: " ".to_owned(),
|
|
width: 40,
|
|
cjk: false,
|
|
space: true,
|
|
pixel_space: true,
|
|
source_template_id: None,
|
|
},
|
|
MeasuredCluster {
|
|
text: "c".to_owned(),
|
|
width: 6,
|
|
cjk: false,
|
|
space: false,
|
|
pixel_space: false,
|
|
source_template_id: None,
|
|
},
|
|
MeasuredCluster {
|
|
text: "d".to_owned(),
|
|
width: 7,
|
|
cjk: false,
|
|
space: false,
|
|
pixel_space: false,
|
|
source_template_id: None,
|
|
},
|
|
];
|
|
let rendered = Rendered::from_lines(vec![Line::from_clusters(&clusters)]);
|
|
|
|
assert_eq!(rendered.min_content_width(WrapMode::Word), 13);
|
|
assert_eq!(rendered.min_content_width(WrapMode::Char), 13);
|
|
assert_eq!(rendered.min_content_width(WrapMode::None), 71);
|
|
}
|
|
|
|
#[test]
|
|
fn border_box_resolution_subtracts_only_box_chrome() {
|
|
assert_eq!(
|
|
box_sizing_content_width(100, BoxSizing::BorderBox, 8, 7, 2, 3),
|
|
80
|
|
);
|
|
assert_eq!(
|
|
box_sizing_content_width(100, BoxSizing::ContentBox, 8, 7, 2, 3),
|
|
100
|
|
);
|
|
assert_eq!(box_sizing_content_height(9, BoxSizing::BorderBox, 2, 1), 6);
|
|
}
|
|
|
|
#[test]
|
|
fn bounded_intrinsic_widths_and_height_expressions_resolve_exactly() {
|
|
let context = LayoutContext {
|
|
viewport_width: 320,
|
|
viewport_width_known: true,
|
|
viewport_height: 10,
|
|
};
|
|
let fit_content = Size::FitContent {
|
|
limit: Some(Box::new(Size::Pixels { value: 180 })),
|
|
};
|
|
assert_eq!(
|
|
resolve_width(
|
|
&fit_content,
|
|
None,
|
|
context,
|
|
BoxSizing::ContentBox,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
Some(320),
|
|
40,
|
|
300,
|
|
),
|
|
Some(180)
|
|
);
|
|
|
|
let height = Size::Subtract {
|
|
values: Box::new(SizeValues(
|
|
vec![Size::ViewportHeight, Size::Lines { value: 2 }].into_boxed_slice(),
|
|
)),
|
|
};
|
|
assert_eq!(
|
|
resolve_height(&height, None, context, BoxSizing::BorderBox, 1, 1),
|
|
Some(6)
|
|
);
|
|
|
|
let empty = Size::Add {
|
|
values: Box::new(SizeValues(Vec::new().into_boxed_slice())),
|
|
};
|
|
assert!(validate_size("height", &empty).is_err());
|
|
|
|
let unavailable = LayoutContext {
|
|
viewport_width: 0,
|
|
viewport_width_known: false,
|
|
viewport_height: 0,
|
|
};
|
|
assert_eq!(
|
|
resolve_width(
|
|
&Size::Viewport,
|
|
None,
|
|
unavailable,
|
|
BoxSizing::ContentBox,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
None,
|
|
40,
|
|
300,
|
|
),
|
|
None
|
|
);
|
|
assert_eq!(resolve_raw_height(&Size::ViewportHeight, unavailable), None);
|
|
|
|
assert_eq!(
|
|
resolve_child_viewport_width(
|
|
&Size::Pixels { value: 300 },
|
|
&Size::Pixels { value: 0 },
|
|
&Size::Viewport,
|
|
LayoutContext {
|
|
viewport_width: 298,
|
|
viewport_width_known: true,
|
|
viewport_height: 20,
|
|
},
|
|
BoxSizing::BorderBox,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
Some(298),
|
|
),
|
|
Some(298)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn zero_width_remains_a_known_nested_flex_constraint() {
|
|
let known_zero = LayoutContext {
|
|
viewport_width: 0,
|
|
viewport_width_known: true,
|
|
viewport_height: 10,
|
|
};
|
|
let unavailable = LayoutContext {
|
|
viewport_width: 0,
|
|
viewport_width_known: false,
|
|
viewport_height: 10,
|
|
};
|
|
|
|
assert_eq!(flex_horizontal_size(&Size::Auto, known_zero), Some(0));
|
|
assert_eq!(flex_horizontal_size(&Size::Viewport, known_zero), Some(0));
|
|
assert_eq!(flex_horizontal_size(&Size::Auto, unavailable), None);
|
|
assert_eq!(flex_horizontal_size(&Size::Viewport, unavailable), None);
|
|
}
|
|
|
|
#[test]
|
|
fn flex_integer_distribution_and_spacing_preserve_elisp_rounding() {
|
|
assert_eq!(flex_distribute(7.0, &[1.0, 2.0, 1.0]), [2, 4, 1]);
|
|
assert_eq!(flex_distribute(3.9, &[1.0, 1.0]), [2, 1]);
|
|
assert_eq!(flex_spacing(FlexAlign::SpaceAround, 11, 3, 2), (1, 5, 4));
|
|
assert_eq!(flex_spacing(FlexAlign::SpaceEvenly, 11, 3, 2), (2, 4, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn flex_partial_fill_and_min_wins_clamping_match_the_oracle() {
|
|
assert_eq!(flex_effective_free_space(200, 200, 0.5), 100.0);
|
|
assert_eq!(flex_effective_free_space(-50, -50, 0.5), -25.0);
|
|
assert_eq!(flex_clamp_main(40, 80, Some(60)), 80);
|
|
assert_eq!(flex_clamp_main(-5, 0, None), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_stack_and_scroll_preserve_break_metadata() {
|
|
let mut first = Rendered::from_lines(vec![Line::blank(1), Line::blank(1)]);
|
|
first.breaks[0].owner = Some(11);
|
|
let second = Rendered::from_lines(vec![Line::blank(1)]);
|
|
let mut stacked = stack_vertical(vec![first, second]);
|
|
|
|
assert_eq!(stacked.breaks.len(), 2);
|
|
assert_eq!(stacked.breaks[0].owner, Some(11));
|
|
assert_eq!(stacked.breaks[1], AtomProperties::default());
|
|
|
|
stacked.apply_scroll_window(9);
|
|
assert!(stacked
|
|
.breaks
|
|
.iter()
|
|
.all(|properties| { properties.scroll_window == Some(9) }));
|
|
assert!(stacked.lines.iter().all(|line| {
|
|
line.atoms
|
|
.iter()
|
|
.all(|atom| atom.properties().scroll_window == Some(9))
|
|
}));
|
|
|
|
let tape = stacked.into_tape(0);
|
|
assert_eq!(tape.lines[0].break_after.as_ref().unwrap().owner, Some(11));
|
|
assert_eq!(
|
|
tape.lines[1].break_after.as_ref().unwrap().scroll_window,
|
|
Some(9)
|
|
);
|
|
assert!(tape.lines[2].break_after.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn binary_tapes_are_versioned_bounded_and_support_light_frames() {
|
|
let properties = AtomProperties {
|
|
style_ids: vec![2],
|
|
content: Some(11),
|
|
content_idx: Some(0),
|
|
owner: Some(11),
|
|
owners: vec![11],
|
|
roles: vec![RegionRoleEntry {
|
|
role: RegionRole::PaddingLeft,
|
|
region_id: 11,
|
|
}],
|
|
scroll_window: Some(11),
|
|
property_template_ids: vec![4],
|
|
};
|
|
let mut rendered = Rendered::from_lines(vec![
|
|
Line::blank_with_properties(8, properties.clone()),
|
|
Line::blank_with_properties(8, properties.clone()),
|
|
]);
|
|
rendered.breaks[0] = properties;
|
|
let tape = rendered.into_tape(3);
|
|
let styles = vec![
|
|
StyleTemplate {
|
|
mode: StyleMode::Add,
|
|
face: FaceTemplate {
|
|
lisp: None,
|
|
inherit: None,
|
|
inverse_video: None,
|
|
foreground: Some("red".to_owned()),
|
|
background: None,
|
|
overline: None,
|
|
underline: None,
|
|
},
|
|
},
|
|
StyleTemplate {
|
|
mode: StyleMode::Add,
|
|
face: FaceTemplate {
|
|
lisp: None,
|
|
inherit: None,
|
|
inverse_video: None,
|
|
foreground: None,
|
|
background: Some("blue".to_owned()),
|
|
overline: None,
|
|
underline: None,
|
|
},
|
|
},
|
|
StyleTemplate {
|
|
mode: StyleMode::Set,
|
|
face: FaceTemplate {
|
|
lisp: None,
|
|
inherit: None,
|
|
inverse_video: Some(true),
|
|
foreground: Some("white".to_owned()),
|
|
background: None,
|
|
overline: None,
|
|
underline: None,
|
|
},
|
|
},
|
|
];
|
|
let identity = TapeIdentity {
|
|
session_id: 7,
|
|
generation: 9,
|
|
key: 13,
|
|
runtime_revision: 17,
|
|
context_hash: -19,
|
|
viewport_width: 320,
|
|
viewport_height: 40,
|
|
root_width: 300,
|
|
complete: true,
|
|
};
|
|
|
|
let full = encode_layout_tape(tape.clone(), &styles, identity, true, 4096).unwrap();
|
|
assert_eq!(&full[..4], TAPE_MAGIC);
|
|
assert_eq!(
|
|
u16::from_le_bytes(full[4..6].try_into().unwrap()),
|
|
TAPE_VERSION
|
|
);
|
|
assert_eq!(u16::from_le_bytes(full[6..8].try_into().unwrap()), 3);
|
|
assert_eq!(
|
|
u64::from_le_bytes(full[12..20].try_into().unwrap()) as usize,
|
|
full.len()
|
|
);
|
|
assert_eq!(u64::from_le_bytes(full[20..28].try_into().unwrap()), 7);
|
|
assert_eq!(u32::from_le_bytes(full[84..88].try_into().unwrap()), 3);
|
|
assert_eq!(u32::from_le_bytes(full[88..92].try_into().unwrap()), 2);
|
|
assert_eq!(u64::from_le_bytes(full[92..100].try_into().unwrap()), 3);
|
|
let literal_length = u64::from_le_bytes(full[112..120].try_into().unwrap()) as usize;
|
|
assert_eq!(u32::from_le_bytes(full[120..124].try_into().unwrap()), 2);
|
|
let metadata_length = u32::from_le_bytes(full[124..128].try_into().unwrap()) as usize;
|
|
let metadata_records = u64::from_le_bytes(full[128..136].try_into().unwrap()) as usize;
|
|
let fragment_length = u64::from_le_bytes(full[136..144].try_into().unwrap()) as usize;
|
|
let fragment_records = u64::from_le_bytes(full[144..152].try_into().unwrap()) as usize;
|
|
assert!(metadata_length > 0);
|
|
assert!(metadata_records > 0);
|
|
assert!(fragment_length > 0);
|
|
assert!(fragment_records > 0);
|
|
assert_eq!(u64::from_le_bytes(full[152..160].try_into().unwrap()), 8);
|
|
assert_eq!(u64::from_le_bytes(full[160..168].try_into().unwrap()), 8);
|
|
let literal_start = 168;
|
|
let literal_end = literal_start + literal_length;
|
|
let literal = std::str::from_utf8(&full[literal_start..literal_end]).unwrap();
|
|
assert_eq!(literal.len(), literal_length);
|
|
assert!(literal.starts_with("#(\" \\n \""));
|
|
assert!(literal.contains("face #3=(:inverse-video t :foreground \"white\")"));
|
|
assert!(literal.contains("face #3#"));
|
|
assert!(literal.contains("ebox-content 11"));
|
|
assert!(literal.contains("ebox-native-property-template-ids (4)"));
|
|
assert!(literal.contains("display (space :width (8))"));
|
|
let metadata = &full[literal_end..literal_end + metadata_length];
|
|
assert_eq!(metadata.len(), metadata_length);
|
|
let metadata_literal = metadata_from_full_tape(&full);
|
|
assert!(metadata_literal.starts_with("(:prepared-p t"));
|
|
assert!(metadata_literal.contains("#s(hash-table test equal data"));
|
|
assert!(metadata_literal.contains("(11 content) ((1 . 4))"));
|
|
assert!(metadata_literal.contains("(11 pl) ((1 . 4))"));
|
|
assert!(metadata_literal.contains("data (11 (1 . 4))"));
|
|
assert!(metadata_literal.contains("data (11 ((0 0 . 3)"));
|
|
assert!(metadata_literal.contains(":scroll-window-p t"));
|
|
assert!(metadata_literal.ends_with(":scroll-window-p t)"));
|
|
|
|
let full_without_root_metadata =
|
|
encode_layout_tape(tape.clone(), &styles, identity, false, 4096).unwrap();
|
|
assert_eq!(
|
|
u16::from_le_bytes(full_without_root_metadata[6..8].try_into().unwrap()),
|
|
3
|
|
);
|
|
assert!(u32::from_le_bytes(full_without_root_metadata[124..128].try_into().unwrap()) > 0);
|
|
assert_eq!(
|
|
u64::from_le_bytes(full_without_root_metadata[128..136].try_into().unwrap()),
|
|
1
|
|
);
|
|
assert!(u64::from_le_bytes(full_without_root_metadata[136..144].try_into().unwrap()) > 0);
|
|
assert!(u64::from_le_bytes(full_without_root_metadata[144..152].try_into().unwrap()) > 0);
|
|
let full_without_root_metadata_literal =
|
|
literal_from_full_tape(&full_without_root_metadata);
|
|
assert!(full_without_root_metadata_literal.contains("ebox-content 11"));
|
|
assert!(
|
|
full_without_root_metadata_literal.contains("ebox-native-property-template-ids (4)")
|
|
);
|
|
|
|
let light = encode_layout_tape(
|
|
tape.clone(),
|
|
&styles,
|
|
TapeIdentity {
|
|
complete: false,
|
|
..identity
|
|
},
|
|
true,
|
|
4096,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(u16::from_le_bytes(light[6..8].try_into().unwrap()), 1);
|
|
assert_eq!(u32::from_le_bytes(light[124..128].try_into().unwrap()), 0);
|
|
assert_eq!(u64::from_le_bytes(light[128..136].try_into().unwrap()), 0);
|
|
assert_eq!(u64::from_le_bytes(light[136..144].try_into().unwrap()), 0);
|
|
assert_eq!(u64::from_le_bytes(light[144..152].try_into().unwrap()), 0);
|
|
assert!(light.len() < full.len());
|
|
assert!(encode_layout_tape(tape, &styles, identity, true, TAPE_HEADER_LEN).is_err());
|
|
|
|
let error = encode_error_tape(identity, &"x".repeat(1024), 160);
|
|
assert!(error.len() <= 160);
|
|
assert_eq!(u16::from_le_bytes(error[6..8].try_into().unwrap()), 2);
|
|
assert_eq!(u32::from_le_bytes(error[88..92].try_into().unwrap()), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn root_metadata_emits_only_safe_box_extents() {
|
|
let text = "abbc";
|
|
let property_spans = vec![
|
|
TapePropertySpan {
|
|
start: 0,
|
|
end: 1,
|
|
properties: AtomProperties {
|
|
content: Some(2),
|
|
owner: Some(2),
|
|
owners: vec![1, 2],
|
|
..AtomProperties::default()
|
|
},
|
|
},
|
|
TapePropertySpan {
|
|
start: 1,
|
|
end: 3,
|
|
properties: AtomProperties {
|
|
content: Some(3),
|
|
owner: Some(3),
|
|
owners: vec![1, 3],
|
|
..AtomProperties::default()
|
|
},
|
|
},
|
|
TapePropertySpan {
|
|
start: 3,
|
|
end: 4,
|
|
properties: AtomProperties {
|
|
content: Some(2),
|
|
owner: Some(2),
|
|
owners: vec![1, 2],
|
|
roles: vec![RegionRoleEntry {
|
|
role: RegionRole::PaddingRight,
|
|
region_id: 2,
|
|
}],
|
|
..AtomProperties::default()
|
|
},
|
|
},
|
|
];
|
|
|
|
let records = build_root_metadata_records(text, &property_spans).unwrap();
|
|
assert!(records.iter().any(|record| {
|
|
record.kind == METADATA_BOX_EXTENT
|
|
&& record.region_id == 1
|
|
&& record.start == 0
|
|
&& record.end == 4
|
|
}));
|
|
assert!(records.iter().any(|record| {
|
|
record.kind == METADATA_BOX_EXTENT
|
|
&& record.region_id == 3
|
|
&& record.start == 1
|
|
&& record.end == 3
|
|
}));
|
|
assert!(!records
|
|
.iter()
|
|
.any(|record| record.kind == METADATA_BOX_EXTENT && record.region_id == 2));
|
|
assert!(records
|
|
.iter()
|
|
.any(|record| record.kind == METADATA_ROLE_CONTENT && record.region_id == 2));
|
|
}
|
|
|
|
#[test]
|
|
fn root_metadata_literal_preserves_zero_metadata_and_byte_limit() {
|
|
let fragment_only_payload = root_metadata_payload("x", &[], &[], true, true, 4096)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(fragment_only_payload.record_count, 0);
|
|
assert_eq!(fragment_only_payload.fragment_count, 1);
|
|
assert!(!fragment_only_payload.fragment_bytes.is_empty());
|
|
|
|
let property_spans = vec![TapePropertySpan {
|
|
start: 0,
|
|
end: 1,
|
|
properties: AtomProperties {
|
|
content: Some(42),
|
|
owner: Some(42),
|
|
..AtomProperties::default()
|
|
},
|
|
}];
|
|
let payload = root_metadata_payload("x", &[], &property_spans, true, true, 4096)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(payload.record_count, 4);
|
|
assert_eq!(payload.fragment_count, 1);
|
|
assert!(payload.literal.contains("(42 content) ((1 . 2))"));
|
|
assert!(payload.literal.contains("data (42 (1 . 2))"));
|
|
assert!(payload.literal.contains("data (42 ((0 0 . 1)))"));
|
|
assert!(root_metadata_payload(
|
|
"x",
|
|
&[],
|
|
&property_spans,
|
|
true,
|
|
true,
|
|
payload.literal.len() - 1,
|
|
)
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn complete_tape_emits_property_template_ids_and_light_tape_strips_them() {
|
|
let tape = LayoutTape {
|
|
style_count: 0,
|
|
lines: vec![TapeLine {
|
|
width: 1,
|
|
atoms: vec![TapeAtom::Text {
|
|
text: "x".to_owned(),
|
|
width: 1,
|
|
properties: AtomProperties {
|
|
property_template_ids: vec![2, 4],
|
|
..AtomProperties::default()
|
|
},
|
|
}],
|
|
break_after: None,
|
|
}],
|
|
};
|
|
let identity = complete_identity();
|
|
|
|
let full = encode_layout_tape(tape.clone(), &[], identity, true, 4096).unwrap();
|
|
let full_literal = literal_from_full_tape(&full);
|
|
assert!(full_literal.contains("ebox-native-property-template-ids (2 4)"));
|
|
|
|
let light = encode_layout_tape(
|
|
tape,
|
|
&[],
|
|
TapeIdentity {
|
|
complete: false,
|
|
..identity
|
|
},
|
|
true,
|
|
4096,
|
|
)
|
|
.unwrap();
|
|
let light_literal = literal_from_full_tape(&light);
|
|
assert!(!light_literal.contains("ebox-native-property-template-ids"));
|
|
assert!(flatten_layout_tape(
|
|
LayoutTape {
|
|
style_count: 0,
|
|
lines: vec![TapeLine {
|
|
width: 1,
|
|
atoms: vec![TapeAtom::Text {
|
|
text: "x".to_owned(),
|
|
width: 1,
|
|
properties: AtomProperties {
|
|
property_template_ids: vec![2, 4],
|
|
..AtomProperties::default()
|
|
},
|
|
}],
|
|
break_after: None,
|
|
}],
|
|
},
|
|
false,
|
|
)
|
|
.unwrap()
|
|
.characters[0]
|
|
.properties
|
|
.property_template_ids
|
|
.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_literal_property_plists_are_shared_with_read_circle_labels() {
|
|
let properties = AtomProperties {
|
|
content: Some(42),
|
|
property_template_ids: vec![3, 7],
|
|
..AtomProperties::default()
|
|
};
|
|
let literal = encode_lisp_literal_inner(
|
|
"abcd",
|
|
&[],
|
|
&[
|
|
TapePropertySpan {
|
|
start: 0,
|
|
end: 1,
|
|
properties: properties.clone(),
|
|
},
|
|
TapePropertySpan {
|
|
start: 2,
|
|
end: 3,
|
|
properties,
|
|
},
|
|
],
|
|
&[],
|
|
4,
|
|
4096,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
literal,
|
|
"#(\"abcd\" 0 1 #1=(ebox-content 42 ebox-native-property-template-ids (3 7)) 2 3 #1#)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn myers_diff_bounds_pathological_search_memory() {
|
|
let character = |value: char| TapeCharacter {
|
|
value,
|
|
pixel_width: None,
|
|
properties: AtomProperties::default(),
|
|
};
|
|
let old: Vec<TapeCharacter> = (0..30_000).map(|_| character('a')).collect();
|
|
let new: Vec<TapeCharacter> = (0..30_000).map(|_| character('b')).collect();
|
|
let patches = minimal_tape_patches(&old, &new);
|
|
assert_eq!(
|
|
patches,
|
|
vec![TapePatch {
|
|
old_start: 0,
|
|
old_end: 30_000,
|
|
new_start: 0,
|
|
new_end: 30_000,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn myers_diff_trims_common_affixes_before_search() {
|
|
let character = |value: char| TapeCharacter {
|
|
value,
|
|
pixel_width: None,
|
|
properties: AtomProperties::default(),
|
|
};
|
|
let old: Vec<TapeCharacter> = "prefixXsuffix".chars().map(character).collect();
|
|
let new: Vec<TapeCharacter> = "prefixYsuffix".chars().map(character).collect();
|
|
let patches = minimal_tape_patches(&old, &new);
|
|
assert_eq!(
|
|
patches,
|
|
vec![TapePatch {
|
|
old_start: 6,
|
|
old_end: 7,
|
|
new_start: 6,
|
|
new_end: 7,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fixed_height_scroll_layout_does_not_render_offscreen_column_suffix() {
|
|
let children = (0..200)
|
|
.map(|index| {
|
|
let mut node = text_box(
|
|
index + 2,
|
|
measured_text(vec![vec![cluster(&format!("line-{index:03}"), 8, None)]]),
|
|
None,
|
|
);
|
|
let LayoutNode::Box { width, height, .. } = &mut node else {
|
|
unreachable!();
|
|
};
|
|
*width = Size::Viewport;
|
|
*height = Size::Lines { value: 1 };
|
|
node
|
|
})
|
|
.collect();
|
|
let mut root = child_box(1, LayoutNode::Column { children }, None);
|
|
let LayoutNode::Box {
|
|
width,
|
|
height,
|
|
content_width_exact,
|
|
..
|
|
} = &mut root
|
|
else {
|
|
unreachable!();
|
|
};
|
|
*width = Size::Viewport;
|
|
*height = Size::Lines { value: 5 };
|
|
*content_width_exact = false;
|
|
let document = LayoutDocument {
|
|
version: LAYOUT_VERSION,
|
|
space_width: 1,
|
|
style_count: 0,
|
|
property_template_count: 0,
|
|
styles: Vec::new(),
|
|
root,
|
|
};
|
|
document.validate().unwrap();
|
|
|
|
let (tape, rendered_nodes) = counted_layout_tape(&document, test_context());
|
|
|
|
assert_eq!(tape.lines.len(), 5);
|
|
assert!(
|
|
rendered_nodes <= 8,
|
|
"fixed scroll rendered {rendered_nodes} nodes for five visible lines"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fixed_height_scroll_layout_uses_nonzero_offset_window() {
|
|
let document = fixed_scroll_column_document(7);
|
|
document.validate().unwrap();
|
|
|
|
let (tape, rendered_nodes) = counted_layout_tape(&document, test_context());
|
|
let flat = flatten_layout_tape(tape, true).unwrap();
|
|
let text = flat
|
|
.characters
|
|
.iter()
|
|
.map(|character| character.value)
|
|
.collect::<String>();
|
|
|
|
assert_eq!(
|
|
text,
|
|
"line-007 \nline-008 \nline-009 \nline-010 \nline-011 "
|
|
);
|
|
assert!(
|
|
rendered_nodes <= 8,
|
|
"fixed scroll rendered {rendered_nodes} nodes for five visible lines"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fixed_height_scroll_window_matches_eager_tape_exactly() {
|
|
let document = fixed_scroll_column_document(13);
|
|
document.validate().unwrap();
|
|
|
|
let eager = eager_layout_tape(&document, test_context());
|
|
let (windowed, rendered_nodes) = counted_layout_tape(&document, test_context());
|
|
|
|
assert_eq!(windowed, eager);
|
|
assert!(
|
|
rendered_nodes <= 8,
|
|
"fixed scroll rendered {rendered_nodes} nodes for five visible lines"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn native_patch_solver_returns_only_disjoint_semantic_changes() {
|
|
let old = LayoutTape {
|
|
style_count: 0,
|
|
lines: vec![TapeLine {
|
|
width: 6,
|
|
atoms: vec![TapeAtom::Text {
|
|
text: "abcdef".to_owned(),
|
|
width: 6,
|
|
properties: AtomProperties::default(),
|
|
}],
|
|
break_after: None,
|
|
}],
|
|
};
|
|
let target = LayoutTape {
|
|
style_count: 0,
|
|
lines: vec![TapeLine {
|
|
width: 13,
|
|
atoms: vec![
|
|
TapeAtom::Text {
|
|
text: "abX".to_owned(),
|
|
width: 3,
|
|
properties: AtomProperties::default(),
|
|
},
|
|
TapeAtom::Text {
|
|
text: "d".to_owned(),
|
|
width: 1,
|
|
properties: AtomProperties::default(),
|
|
},
|
|
TapeAtom::Text {
|
|
text: "e".to_owned(),
|
|
width: 1,
|
|
properties: AtomProperties {
|
|
content: Some(9),
|
|
..AtomProperties::default()
|
|
},
|
|
},
|
|
TapeAtom::Text {
|
|
text: "f".to_owned(),
|
|
width: 1,
|
|
properties: AtomProperties::default(),
|
|
},
|
|
TapeAtom::Space {
|
|
width: 7,
|
|
properties: AtomProperties::default(),
|
|
},
|
|
],
|
|
break_after: None,
|
|
}],
|
|
};
|
|
|
|
let old = flatten_layout_tape(old, true).unwrap();
|
|
let target = flatten_layout_tape(target, true).unwrap();
|
|
let patches = minimal_tape_patches(&old.characters, &target.characters);
|
|
|
|
assert_eq!(
|
|
patches,
|
|
vec![
|
|
TapePatch {
|
|
old_start: 2,
|
|
old_end: 3,
|
|
new_start: 2,
|
|
new_end: 3,
|
|
},
|
|
TapePatch {
|
|
old_start: 4,
|
|
old_end: 5,
|
|
new_start: 4,
|
|
new_end: 5,
|
|
},
|
|
TapePatch {
|
|
old_start: 6,
|
|
old_end: 6,
|
|
new_start: 6,
|
|
new_end: 7,
|
|
},
|
|
]
|
|
);
|
|
|
|
let identity = TapeIdentity {
|
|
session_id: 1,
|
|
generation: 2,
|
|
key: 3,
|
|
runtime_revision: 4,
|
|
context_hash: 5,
|
|
viewport_width: 140,
|
|
viewport_height: 20,
|
|
root_width: 140,
|
|
complete: true,
|
|
};
|
|
let encoded = encode_layout_patch_tape(
|
|
LayoutTape {
|
|
style_count: old.style_count,
|
|
lines: vec![TapeLine {
|
|
width: 6,
|
|
atoms: vec![TapeAtom::Text {
|
|
text: "abcdef".to_owned(),
|
|
width: 6,
|
|
properties: AtomProperties::default(),
|
|
}],
|
|
break_after: None,
|
|
}],
|
|
},
|
|
LayoutTape {
|
|
style_count: target.style_count,
|
|
lines: vec![TapeLine {
|
|
width: 13,
|
|
atoms: vec![
|
|
TapeAtom::Text {
|
|
text: "abXd".to_owned(),
|
|
width: 4,
|
|
properties: AtomProperties::default(),
|
|
},
|
|
TapeAtom::Text {
|
|
text: "e".to_owned(),
|
|
width: 1,
|
|
properties: AtomProperties {
|
|
content: Some(9),
|
|
property_template_ids: vec![4],
|
|
..AtomProperties::default()
|
|
},
|
|
},
|
|
TapeAtom::Text {
|
|
text: "f".to_owned(),
|
|
width: 1,
|
|
properties: AtomProperties::default(),
|
|
},
|
|
TapeAtom::Space {
|
|
width: 7,
|
|
properties: AtomProperties::default(),
|
|
},
|
|
],
|
|
break_after: None,
|
|
}],
|
|
},
|
|
&[],
|
|
identity,
|
|
true,
|
|
4096,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(u16::from_le_bytes(encoded[6..8].try_into().unwrap()), 7);
|
|
assert_eq!(
|
|
u16::from_le_bytes(encoded[4..6].try_into().unwrap()),
|
|
TAPE_VERSION
|
|
);
|
|
assert_eq!(u64::from_le_bytes(encoded[92..100].try_into().unwrap()), 7);
|
|
assert_eq!(u64::from_le_bytes(encoded[112..120].try_into().unwrap()), 6);
|
|
let patch_count = u32::from_le_bytes(encoded[120..124].try_into().unwrap()) as usize;
|
|
assert_eq!(patch_count, 3);
|
|
let reserved = u32::from_le_bytes(encoded[124..128].try_into().unwrap());
|
|
let metadata_records = u64::from_le_bytes(encoded[128..136].try_into().unwrap());
|
|
let payload_length = u64::from_le_bytes(encoded[136..144].try_into().unwrap()) as usize;
|
|
let fragment_length = u64::from_le_bytes(encoded[144..152].try_into().unwrap()) as usize;
|
|
let fragment_records = u64::from_le_bytes(encoded[152..160].try_into().unwrap()) as usize;
|
|
let coordinate_patch_count =
|
|
u32::from_le_bytes(encoded[160..164].try_into().unwrap()) as usize;
|
|
assert!(coordinate_patch_count > 0);
|
|
let payload_start = 168 + (patch_count + coordinate_patch_count) * 32;
|
|
let payload_end = payload_start + payload_length;
|
|
let payload = std::str::from_utf8(&encoded[payload_start..payload_end]).unwrap();
|
|
assert_eq!(reserved, 0);
|
|
assert!(metadata_records > 0);
|
|
assert!(fragment_length > 0);
|
|
assert!(fragment_records > 0);
|
|
assert_eq!(encoded.len(), payload_end + fragment_length);
|
|
assert!(payload.starts_with("[#(\"Xe \""));
|
|
assert!(payload.contains("1 2 (ebox-content 9 ebox-native-property-template-ids (4))"));
|
|
assert!(payload.contains("2 3 (display (space :width (7)))"));
|
|
assert!(payload.contains("(:prepared-p t"));
|
|
assert!(payload.contains("(9 content) ((5 . 6))"));
|
|
assert!(payload.contains("data (9 (5 . 6))"));
|
|
assert!(payload.contains(":scroll-window-p nil"));
|
|
assert!(payload.ends_with(":scroll-window-p nil)]"));
|
|
}
|
|
|
|
#[test]
|
|
fn mount_projection_reuse_ignores_fragment_splits_but_not_roles() {
|
|
fn fragment(start: u64, end: u64, region_id: i64) -> FragmentTemplate {
|
|
FragmentTemplate {
|
|
start,
|
|
end,
|
|
line: 0,
|
|
roles: vec![("content", region_id), ("content-owner", region_id)],
|
|
content_owner: Some(region_id),
|
|
content_index: None,
|
|
property_template_ids: Vec::new(),
|
|
style_ids: Vec::new(),
|
|
}
|
|
}
|
|
let old = vec![fragment(0, 2, 7)];
|
|
let target = vec![fragment(0, 1, 7), fragment(1, 2, 7)];
|
|
assert_eq!(
|
|
fragment_region_mount_projection(&old),
|
|
fragment_region_mount_projection(&target)
|
|
);
|
|
let mut styled = target.clone();
|
|
styled[1].style_ids = vec![3, 5];
|
|
assert_eq!(
|
|
fragment_style_delta(&target, &styled),
|
|
Some(vec![(1, vec![3, 5])])
|
|
);
|
|
let changed = vec![fragment(0, 2, 8)];
|
|
assert_ne!(
|
|
fragment_region_mount_projection(&old),
|
|
fragment_region_mount_projection(&changed)
|
|
);
|
|
assert!(fragment_style_delta(&old, &changed).is_none());
|
|
}
|
|
}
|