From c0d698cb9f2d909d52e5f14eddf229caca420e23 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 12 Oct 2026 10:03:23 +0000 Subject: [PATCH 01/19] Take #1167's and #1175's inline layout changes out of blitz-dom for now The text backend commits that follow move and replace layout/inline.rs and the Parley TextLayout, which #1167 and #1175 changed. Those parts are taken out here so that they apply as written, and are put back for the shared inline layout once they have. The stylo_taffy part and the rest of #1175 stay. --- packages/blitz-dom/src/layout/inline.rs | 60 +++---------------------- packages/blitz-dom/src/node/node.rs | 3 -- packages/blitz-dom/src/node/text.rs | 4 -- 3 files changed, 5 insertions(+), 62 deletions(-) diff --git a/packages/blitz-dom/src/layout/inline.rs b/packages/blitz-dom/src/layout/inline.rs index 226243ba73..e5d0118ad5 100644 --- a/packages/blitz-dom/src/layout/inline.rs +++ b/packages/blitz-dom/src/layout/inline.rs @@ -18,8 +18,6 @@ use parley::YieldData; #[cfg(feature = "floats")] use taffy::{BlockItemStyle as _, Clear, Float, prelude::TaffyMaxContent}; -use stylo_taffy::StyleFlags; - use super::resolve_calc_value; use crate::layout::LayoutPassState; use crate::stylo_to_parley; @@ -194,39 +192,6 @@ impl LayoutPassState<'_> { } } - /// The out-of-flow positions for which an inline span between the inline box `box_id` and its - /// inline root `root_id` is a containing block (as `SPAN_*_CB` flags) - fn inline_span_cb_flags(&self, root_id: NodeId, box_id: NodeId) -> StyleFlags { - let root = &self.nodes[root_id]; - // The spans of an anonymous inline root are descendants of its parent - let root_parent = root.is_anonymous().then_some(root.parent).flatten(); - - let mut flags = StyleFlags::empty(); - let mut current = self.nodes[box_id].parent; - while let Some(id) = current { - if id == root_id || Some(id) == root_parent { - return flags; - } - let ancestor = &self.nodes[id]; - if let Some(style) = ancestor.primary_styles() { - let display = style.clone_display(); - if display.outside() == DisplayOutside::Inline - && display.inside() == DisplayInside::Flow - { - let claims = stylo_taffy::convert::inline_containing_block_claims(&style); - if claims.absolute { - flags |= StyleFlags::SPAN_ABSOLUTE_CB; - } - if claims.fixed { - flags |= StyleFlags::SPAN_FIXED_CB; - } - } - } - current = ancestor.parent; - } - StyleFlags::empty() - } - fn compute_inline_layout_inner( &mut self, node_id: NodeId, @@ -325,7 +290,6 @@ impl LayoutPassState<'_> { } }; - let suppresses_baseline = style.contain().suppresses_baseline(); drop(style); // Short circuit if inline context contains no text or inline boxes @@ -409,7 +373,7 @@ impl LayoutPassState<'_> { let is_out_of_flow = style.position().is_out_of_flow(); // The baseline of an inline-block is the baseline of its last in-flow line box, - // unless it has no line boxes or it is a block-axis scroll container (except buttons), in which + // unless it has no line boxes or it is a block-axis scroll container, in which // case it is the bottom margin edge (CSS 2 §10.8.1, css-align-3 §9.1 // `baseline-source: auto`; `overflow: clip` is not a scroll container). Other // atomic inlines (flex, grid, table) export a baseline regardless of `overflow`, @@ -418,15 +382,11 @@ impl LayoutPassState<'_> { let overflow = style.overflow(); let box_style = style.style.get_box(); let is_flow = matches!( - style.effective_display().inside(), + box_style.display.inside(), DisplayInside::Flow | DisplayInside::FlowRoot ); let is_scroll_container = !matches!(overflow.y, Overflow::Visible | Overflow::Clip); - let is_block_axis_scroll_container = is_flow - && is_scroll_container - && !style - .flags - .intersects(StyleFlags::IS_BUTTON | StyleFlags::IS_BUTTON_INPUT); + let is_block_axis_scroll_container = is_flow && is_scroll_container; let contain_layout = box_style.clone_contain().contains(Contain::LAYOUT); let exports_baseline = !is_block_axis_scroll_container && !contain_layout; let box_inputs = inline_box_inputs(style.size().width, margin, child_inputs); @@ -910,16 +870,11 @@ impl LayoutPassState<'_> { // Store sizes and positions of inline boxes let mut ibox_order: u32 = 0; - let mut span_cb_flags = StyleFlags::empty(); for line in inline_layout.layout.lines() { for item in line.items() { if let parley::layout::PositionedLayoutItem::InlineBox(ibox) = item { let order = ibox_order; ibox_order += 1; - if inputs.run_mode == RunMode::PerformLayout { - span_cb_flags |= - self.inline_span_cb_flags(node_id, NodeId::from_u64(ibox.id)); - } let node = &self.nodes[NodeId::from_u64(ibox.id)]; let style = self.child_layout_style(node); let padding = style @@ -1095,20 +1050,15 @@ impl LayoutPassState<'_> { let line_baseline = |line: parley::Line<'_, _>| (line.metrics().baseline / scale) + line_box_top; - let exports_baseline = has_inline_content && !suppresses_baseline; - let first_baseline = exports_baseline + let first_baseline = has_inline_content .then(|| inline_layout.layout.lines().next().map(line_baseline)) .flatten(); let last_line_index = trailing_empty_line.unwrap_or(line_count).checked_sub(1); - let last_baseline = exports_baseline + let last_baseline = has_inline_content .then(|| last_line_index.and_then(|i| inline_layout.layout.get(i))) .flatten() .map(line_baseline); - if inputs.run_mode == RunMode::PerformLayout { - inline_layout.span_cb_flags = span_cb_flags; - } - // Put layout back self.nodes[node_id] .data diff --git a/packages/blitz-dom/src/node/node.rs b/packages/blitz-dom/src/node/node.rs index 66190f49fa..a8a06790f2 100644 --- a/packages/blitz-dom/src/node/node.rs +++ b/packages/blitz-dom/src/node/node.rs @@ -1131,9 +1131,6 @@ impl Node { if el.is_button_input() { flags |= stylo_taffy::StyleFlags::IS_BUTTON_INPUT; } - if let Some(inline_layout) = el.inline_layout_data.as_deref() { - flags |= inline_layout.span_cb_flags; - } } #[cfg(feature = "writing-mode")] diff --git a/packages/blitz-dom/src/node/text.rs b/packages/blitz-dom/src/node/text.rs index 61906af145..d920b821e3 100644 --- a/packages/blitz-dom/src/node/text.rs +++ b/packages/blitz-dom/src/node/text.rs @@ -29,10 +29,6 @@ pub struct TextLayout { /// Block-axis offset (in CSS px) of the line boxes from the top of the container's content /// box, as applied by `align-content`. pub block_offset: f32, - /// The out-of-flow positions for which some inline span with an inline box below it is a - /// containing block (as `SPAN_*_CB` flags). The inline root claims those boxes in its place. - /// Set by each `RunMode::PerformLayout` run of inline layout. - pub span_cb_flags: stylo_taffy::StyleFlags, } impl TextLayout { From 039d0f0902816cbbae2ff2110dcca64b644020d5 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 8 Oct 2026 20:39:59 -0400 Subject: [PATCH 02/19] Fix blitz-dom's rustdoc link warnings A link to Node::style, which does not exist, now names Node::primary_styles; offset_parent's docs no longer link to a private method; a bare URL becomes an automatic link, and two links drop a redundant target. --- packages/blitz-dom/src/layout/replaced.rs | 2 +- packages/blitz-dom/src/node/node.rs | 5 ++--- packages/blitz-dom/src/traversal.rs | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/blitz-dom/src/layout/replaced.rs b/packages/blitz-dom/src/layout/replaced.rs index ce8b2b1348..0da3ced59a 100644 --- a/packages/blitz-dom/src/layout/replaced.rs +++ b/packages/blitz-dom/src/layout/replaced.rs @@ -17,7 +17,7 @@ pub(crate) fn is_replaced_element(tag_name: &LocalName) -> bool { } /// The intrinsic dimensions of a replaced element per CSS Images 3 -/// (https://drafts.csswg.org/css-images/#intrinsic-dimensions): an intrinsic +/// (): an intrinsic /// width, an intrinsic height, and an intrinsic aspect ratio, each of which /// may independently be absent. #[derive(Debug, Clone, Copy, Default)] diff --git a/packages/blitz-dom/src/node/node.rs b/packages/blitz-dom/src/node/node.rs index a8a06790f2..2845fb00ab 100644 --- a/packages/blitz-dom/src/node/node.rs +++ b/packages/blitz-dom/src/node/node.rs @@ -128,7 +128,7 @@ pub struct Node { /// For element nodes this holds the [`ElementData`], which stores most of /// the per-node style/layout state. For the document node it holds the /// [`DocumentData`]. Access the moved fields through the forwarding methods - /// on [`Node`] (e.g. [`Node::style`], [`Node::final_layout`]). + /// on [`Node`] (e.g. [`Node::primary_styles`], [`Node::final_layout`]). pub data: NodeData, } @@ -1678,8 +1678,7 @@ impl Node { .is_some_and(|styles| styles.get_box().position == Position::Static) } - /// The nearest layout ancestor that [is an offset parent](Self::is_offset_parent), as in - /// CSSOM View's `offsetParent`. + /// The nearest layout ancestor that is an offset parent, as in CSSOM View's `offsetParent`. pub fn offset_parent(&self) -> Option<&Node> { let mut node = self; loop { diff --git a/packages/blitz-dom/src/traversal.rs b/packages/blitz-dom/src/traversal.rs index bbab931581..88c124237a 100644 --- a/packages/blitz-dom/src/traversal.rs +++ b/packages/blitz-dom/src/traversal.rs @@ -44,7 +44,7 @@ macro_rules! iter_children_and_pseudos { pub(crate) use iter_children_and_pseudos; #[derive(Clone)] -/// An pre-order tree traverser for a [BaseDocument](crate::document::BaseDocument). +/// An pre-order tree traverser for a [BaseDocument]. pub struct TreeTraverser<'a> { doc: &'a BaseDocument, stack: Vec, @@ -75,7 +75,7 @@ impl Iterator for TreeTraverser<'_> { } #[derive(Clone)] -/// An ancestor traverser for a [BaseDocument](crate::document::BaseDocument). +/// An ancestor traverser for a [BaseDocument]. pub struct AncestorTraverser<'a> { doc: &'a BaseDocument, current: NodeId, From faf121fa3847e1be3450bc4f9e5a1f2ecb541394 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 8 Oct 2026 09:04:17 -0400 Subject: [PATCH 03/19] Move the Parley text path behind a text backend boundary blitz-dom gains a text module that holds what Blitz asks of a text backend: the fonts documents lay text out with (TextFonts), a document's text context (DocumentText), an inline formatting context's layout and readback (InlineText, InlineLayoutEngine), form-control editing (EditableText, EditEngine, with an Edit enum of editing operations) and outside list markers (MarkerEngine). Aliases name the backend's types: FontContext, TextLayout, TextEditor, MarkerLayout and FaceDescriptors. text::parley implements them with the code that was spread through the crate: building the Parley layout, breaking and placing its lines, PlainEditor, the font metrics provider, @font-face registration and the Stylo conversions. The rest of Blitz now goes through the boundary: the document keeps one TextContext; deferred construction and inline layout call the backend; the key, IME and pointer handling of text inputs speak Edit; hit testing, selected text, innerText, Selection offsets and inline fragment rects read InlineText. blitz-paint's text painting splits into the decorations any backend's lines share and a Parley module behind paint_inline_layout, paint_text_input and paint_marker, selected by blitz-dom's cfg_text_backend! macro. Behaviour is unchanged. --- apps/browser/src/document_loader.rs | 6 +- packages/blitz-dom/build.rs | 6 + packages/blitz-dom/src/config.rs | 7 +- packages/blitz-dom/src/debug.rs | 45 +- packages/blitz-dom/src/document.rs | 187 +--- packages/blitz-dom/src/events/ime.rs | 6 +- packages/blitz-dom/src/events/keyboard.rs | 18 +- packages/blitz-dom/src/events/pointer.rs | 38 +- packages/blitz-dom/src/font_metrics.rs | 177 ---- packages/blitz-dom/src/form.rs | 3 +- packages/blitz-dom/src/iframe.rs | 3 +- packages/blitz-dom/src/layout/construct.rs | 360 +------- packages/blitz-dom/src/layout/damage.rs | 8 +- packages/blitz-dom/src/layout/inline.rs | 833 +----------------- packages/blitz-dom/src/layout/list.rs | 46 +- packages/blitz-dom/src/layout/mod.rs | 11 +- .../blitz-dom/src/layout/text_transform.rs | 29 +- packages/blitz-dom/src/lib.rs | 38 +- packages/blitz-dom/src/mutator.rs | 12 +- packages/blitz-dom/src/net.rs | 98 +-- packages/blitz-dom/src/node/element.rs | 148 +--- packages/blitz-dom/src/node/mod.rs | 2 +- packages/blitz-dom/src/node/node.rs | 166 +--- packages/blitz-dom/src/node/text.rs | 323 ++++--- packages/blitz-dom/src/resolve.rs | 102 +-- packages/blitz-dom/src/stylo_device.rs | 10 +- packages/blitz-dom/src/text/mod.rs | 387 ++++++++ packages/blitz-dom/src/text/parley/build.rs | 358 ++++++++ packages/blitz-dom/src/text/parley/editor.rs | 328 +++++++ packages/blitz-dom/src/text/parley/fonts.rs | 471 ++++++++++ packages/blitz-dom/src/text/parley/layout.rs | 825 +++++++++++++++++ packages/blitz-dom/src/text/parley/mod.rs | 416 +++++++++ .../parley/style.rs} | 2 +- packages/blitz-paint/src/render.rs | 125 +-- .../blitz-paint/src/{text.rs => text/mod.rs} | 258 +----- packages/blitz-paint/src/text/parley.rs | 410 +++++++++ .../blitz-vibey-script/src/dom/element.rs | 5 +- .../blitz-vibey-script/src/dom/selection.rs | 56 +- packages/blitz-vibey-script/src/inner_text.rs | 57 +- packages/blitz-vibey-script/tests/preact.rs | 1 + tests/blitz-tests/tests/harness_smoke.rs | 1 + wpt/runner/src/main.rs | 6 +- 42 files changed, 3703 insertions(+), 2685 deletions(-) create mode 100644 packages/blitz-dom/build.rs delete mode 100644 packages/blitz-dom/src/font_metrics.rs create mode 100644 packages/blitz-dom/src/text/mod.rs create mode 100644 packages/blitz-dom/src/text/parley/build.rs create mode 100644 packages/blitz-dom/src/text/parley/editor.rs create mode 100644 packages/blitz-dom/src/text/parley/fonts.rs create mode 100644 packages/blitz-dom/src/text/parley/layout.rs create mode 100644 packages/blitz-dom/src/text/parley/mod.rs rename packages/blitz-dom/src/{stylo_to_parley.rs => text/parley/style.rs} (99%) rename packages/blitz-paint/src/{text.rs => text/mod.rs} (66%) create mode 100644 packages/blitz-paint/src/text/parley.rs diff --git a/apps/browser/src/document_loader.rs b/apps/browser/src/document_loader.rs index c18ffb95d6..e7a70df8bb 100644 --- a/apps/browser/src/document_loader.rs +++ b/apps/browser/src/document_loader.rs @@ -1,5 +1,6 @@ use std::sync::{Arc, Mutex}; +use blitz_dom::text::TextFonts as _; use blitz_dom::{DocumentConfig, FontContext}; use blitz_html::{HtmlDocument, HtmlProvider}; use blitz_traits::{ @@ -7,7 +8,6 @@ use blitz_traits::{ shell::ShellProvider, }; use dioxus_native::{SubDocumentAttr, prelude::*}; -use linebender_resource_handle::Blob; use crate::StdNetProvider; use crate::favicon::favicon_candidate; @@ -67,9 +67,7 @@ pub fn make_doc_config( impl DocumentLoader { pub fn new(net_provider: Arc, history: SyncStore) -> Self { let mut font_ctx = FontContext::default(); - font_ctx - .collection - .register_fonts(Blob::new(Arc::new(blitz_dom::BULLET_FONT) as _), None); + font_ctx.add_fonts(blitz_dom::BULLET_FONT); Self { font_ctx, diff --git a/packages/blitz-dom/build.rs b/packages/blitz-dom/build.rs new file mode 100644 index 0000000000..15fe302c58 --- /dev/null +++ b/packages/blitz-dom/build.rs @@ -0,0 +1,6 @@ +//! Reads the text backend into the `text_parley` cfg, which the code reads in its place. + +fn main() { + println!("cargo::rustc-check-cfg=cfg(text_parley)"); + println!("cargo::rustc-cfg=text_parley"); +} diff --git a/packages/blitz-dom/src/config.rs b/packages/blitz-dom/src/config.rs index f1a9a5edf8..2ff70e6466 100644 --- a/packages/blitz-dom/src/config.rs +++ b/packages/blitz-dom/src/config.rs @@ -1,10 +1,10 @@ use crate::HtmlParserProvider; +use crate::text::FontContext; use blitz_traits::{ navigation::NavigationProvider, net::{AbortSignal, NetProvider}, shell::{ShellProvider, Viewport}, }; -use parley::FontContext; use std::sync::Arc; use style::media_queries::MediaType; @@ -45,7 +45,10 @@ pub struct DocumentConfig { pub shell_provider: Option>, /// HTML parser provider. Used to parse HTML for setInnerHTML pub html_parser_provider: Option>, - /// Parley `FontContext` + /// The fonts the document lays out text with. Defaults to the platform's fonts, where Blitz + /// is built to read them (`system-fonts`), and Blitz's bullet font. Clones share loaded + /// fonts, so hand a clone of one to every document rather than listing the platform's fonts + /// for each. pub font_ctx: Option, /// The CSS media type used to evaluate `@media` rules. /// Defaults to [`MediaType::screen`]. diff --git a/packages/blitz-dom/src/debug.rs b/packages/blitz-dom/src/debug.rs index b589b80ede..c032392288 100644 --- a/packages/blitz-dom/src/debug.rs +++ b/packages/blitz-dom/src/debug.rs @@ -1,8 +1,8 @@ use blitz_traits::node_id::NodeId; -use parley::layout::PositionedLayoutItem; use crate::BaseDocument; use crate::layout::TaffyDebugTree; +use crate::text::InlineText as _; impl BaseDocument { pub fn print_taffy_tree(&self) { @@ -42,48 +42,7 @@ impl BaseDocument { .as_ref() .unwrap(); - println!( - "Size: {}x{}", - inline_layout.layout.width(), - inline_layout.layout.height() - ); - println!("Text content: {:?}", inline_layout.text); - println!("Inline Boxes:"); - for ibox in inline_layout.layout.inline_boxes() { - print!("(id: {}) ", ibox.id); - } - println!(); - println!("Lines:"); - for (i, line) in inline_layout.layout.lines().enumerate() { - let metrics = line.metrics(); - let x = metrics.inline_min_coord; - let y = metrics.block_min_coord; - let w = metrics.inline_max_coord - metrics.inline_min_coord; - let h = metrics.block_max_coord - metrics.block_min_coord; - println!("Line {i}: x:{x} y:{y} width:{w} height:{h}"); - for item in line.items() { - print!(" "); - match item { - PositionedLayoutItem::GlyphRun(run) => { - print!( - "RUN (x: {}, w: {}) ", - run.offset().round(), - run.run().advance() - ) - } - PositionedLayoutItem::InlineBox(ibox) => print!( - "BOX {:?} (id: {} x: {} y: {} w: {}, h: {})", - ibox.kind, - ibox.id, - ibox.x.round(), - ibox.y.round(), - ibox.width.round(), - ibox.height.round() - ), - } - println!(); - } - } + inline_layout.debug_print(); } let layout = node.final_layout(); diff --git a/packages/blitz-dom/src/document.rs b/packages/blitz-dom/src/document.rs index 5331a8918b..3561e25280 100644 --- a/packages/blitz-dom/src/document.rs +++ b/packages/blitz-dom/src/document.rs @@ -6,11 +6,12 @@ use crate::mutator::ViewportMut; use crate::net::{ Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader, }; -use crate::node::{ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush}; +use crate::node::{ImageData, NodeFlags, RasterImageData, SpecialElementData, Status}; use crate::scrolling::ScrollAnimationState; use crate::selection::TextSelection; use crate::stylo_device::{DeviceChanges, make_device}; use crate::stylo_to_cursor_icon::stylo_to_cursor_icon; +use crate::text::{DocumentText as _, InlineText as _, TextContext, TextInputDriver}; use crate::traversal::TreeTraverser; use crate::url::DocumentUrl; use crate::util::ImageType; @@ -26,9 +27,7 @@ use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request}; use blitz_traits::node_id::NodeId; use blitz_traits::shell::{DummyShellProvider, ShellProvider, Viewport}; use cursor_icon::CursorIcon; -use linebender_resource_handle::Blob; use markup5ever::{LocalName, local_name}; -use parley::{FontContext, PlainEditorDriver}; use selectors::{Element, matching::QuirksMode}; use smallvec::SmallVec; use std::any::Any; @@ -39,7 +38,7 @@ use std::rc::Rc; use std::str::FromStr; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc::{Receiver, Sender, channel}; -use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard}; +use std::sync::{Arc, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard}; use std::task::{Context as TaskContext, Waker}; use style::Atom; use style::animation::DocumentAnimationSet; @@ -69,9 +68,6 @@ use thin_vec::ThinVec; use url::Url; use web_time::Instant; -#[cfg(feature = "parallel-construct")] -use thread_local::ThreadLocal; - pub enum DocGuard<'a> { Ref(&'a BaseDocument), RefCell(std::cell::Ref<'a, BaseDocument>), @@ -239,14 +235,8 @@ pub struct BaseDocument { /// Stylo invalidation map. We insert into this map prior to mutating nodes. pub(crate) snapshots: SnapshotMap, - // Parley contexts - /// A Parley font context - pub(crate) font_ctx: Arc>, - #[cfg(feature = "parallel-construct")] - /// Thread-and-document-local copies to the font context - pub(crate) thread_font_contexts: ThreadLocal>>, - /// A Parley layout context - pub(crate) layout_ctx: parley::LayoutContext, + /// The document's fonts, and the contexts the text backend lays out text in + pub(crate) text: TextContext, /// The real (non-anonymous) node which is currently hovered (if any). /// This is never a layout-generated (anonymous) node, so it remains valid @@ -366,31 +356,7 @@ impl BaseDocument { let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst); - let font_ctx = config - .font_ctx - .map(|mut font_ctx| { - font_ctx.source_cache.make_shared(); - // font_ctx.collection.make_shared(); - font_ctx - }) - .unwrap_or_else(|| { - use parley::fontique::{Collection, CollectionOptions, SourceCache}; - let mut font_ctx = FontContext { - source_cache: SourceCache::new_shared(), - collection: Collection::new(CollectionOptions { - shared: false, - system_fonts: cfg!(all( - feature = "system-fonts", - not(target_arch = "wasm32") - )), - }), - }; - font_ctx - .collection - .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None); - font_ctx - }); - let font_ctx = Arc::new(Mutex::new(font_ctx)); + let text = TextContext::new(config.font_ctx); // Make sure we turn on stylo features *before* creating the Stylist style_config::set_pref!("layout.grid.enabled", true); @@ -407,7 +373,7 @@ impl BaseDocument { let viewport = config.viewport.unwrap_or_default(); let media_type = config.media_type.unwrap_or_else(MediaType::screen); - let device = make_device(&viewport, media_type.clone(), font_ctx.clone()); + let device = make_device(&viewport, media_type.clone(), text.font_metrics_provider()); let stylist = Stylist::new(device, QuirksMode::NoQuirks); let snapshots = SnapshotMap::new(); let nodes = Box::new(NodeTree::new()); @@ -457,10 +423,7 @@ impl BaseDocument { ua_stylesheets: HashMap::new(), nodes_to_stylesheet: BTreeMap::new(), stylesheet_generation: 0, - font_ctx, - #[cfg(feature = "parallel-construct")] - thread_font_contexts: ThreadLocal::new(), - layout_ctx: parley::LayoutContext::new(), + text, hover_node_id: None, hover_hit_node_id: None, @@ -1318,40 +1281,7 @@ impl BaseDocument { self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html); } Resource::Font(bytes, overrides) => { - let font = Blob::new(Arc::new(bytes)); - - // Build a `FontInfoOverride` from the `@font-face` descriptors - // captured during stylesheet parsing. Without this, parley - // reads the family name from the TTF's own metadata, which - // means CSS `font-family: 'Avenir Book'` won't match a font - // file that internally identifies as `Avenir 45 Book`. - let weight_override = overrides.weight.map(parley::fontique::FontWeight::new); - let info_override = parley::fontique::FontInfoOverride { - family_name: overrides.family_name.as_deref(), - weight: weight_override, - style: overrides.style, - ..Default::default() - }; - - // TODO: Investigate eliminating double-box - let mut global_font_ctx = self.font_ctx.lock().unwrap(); - global_font_ctx - .collection - .register_fonts(font.clone(), Some(info_override)); - - #[cfg(feature = "parallel-construct")] - { - rayon::broadcast(|_ctx| { - let mut font_ctx = self - .thread_font_contexts - .get_or(|| RefCell::new(Box::new(global_font_ctx.clone()))) - .borrow_mut(); - font_ctx - .collection - .register_fonts(font.clone(), Some(info_override)); - }); - } - drop(global_font_ctx); + self.text.add_web_font(bytes, &overrides); // TODO: see if we can only invalidate if resolved fonts may have changed self.invalidate_inline_contexts(); @@ -2070,7 +2000,7 @@ impl BaseDocument { self.set_stylist_device(make_device( &self.viewport, self.media_type.clone(), - self.font_ctx.clone(), + self.text.font_metrics_provider(), )); self.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset @@ -2415,11 +2345,8 @@ impl BaseDocument { self.find_element_by_tag_name(&local_name!("title")) } - pub fn with_text_input( - &mut self, - node_id: NodeId, - cb: impl FnOnce(PlainEditorDriver), - ) { + /// Calls `cb` with the editor of the text input at `node_id`, if it is one. + pub fn with_text_input(&mut self, node_id: NodeId, cb: impl FnOnce(TextInputDriver<'_>)) { let Some(node) = self.nodes.get_mut(node_id) else { return; }; @@ -2428,10 +2355,10 @@ impl BaseDocument { .element_data_mut() .and_then(|el| el.text_input_data_mut()) { - let mut font_ctx = self.font_ctx.lock().unwrap(); - let layout_ctx = &mut self.layout_ctx; - let driver = text_input.editor.driver(&mut font_ctx, layout_ctx); - cb(driver) + cb(TextInputDriver { + editor: &mut text_input.editor, + cx: &mut self.text, + }) } } @@ -2610,14 +2537,13 @@ impl BaseDocument { let element_data = node.element_data()?; let inline_layout = element_data.inline_layout_data.as_ref()?; - if *end > inline_layout.text.len() { + let Some(selected) = inline_layout.selected_text(*start, *end) else { continue; - } - + }; if !result.is_empty() { result.push(' '); } - result.push_str(&inline_layout.text[*start..*end]); + result.push_str(&selected); } if result.is_empty() { @@ -2713,7 +2639,7 @@ impl BaseDocument { continue; }; - let text_len = inline_layout.text.len(); + let text_len = inline_layout.text_len(); if node_id == first_node && node_id == last_node { let start = first_offset.min(last_offset); @@ -3266,76 +3192,3 @@ mod hover_invalidation_tests { ); } } - -#[cfg(test)] -mod font_face_override_tests { - use super::*; - use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse}; - - /// Regression-pin for the `@font-face` descriptor-honouring fix. - /// - /// The bug was that `Resource::Font` carried only the raw font bytes, - /// so `load_resource` registered fonts with `info_override = None` and - /// parley fell back to the TTF's internal `name` table. After the fix, - /// `Resource::Font` carries `FontFaceOverrides` and `load_resource` - /// builds a `FontInfoOverride` from them — meaning a CSS-declared - /// `font-family` alias wins over the file's own metadata. - /// - /// We drive `load_resource` directly with a fabricated response rather - /// than go through HTML parsing → `fetch_font_face`, because the - /// downstream HTML parser lives in `blitz-html` (would be a circular - /// crate dependency). The mapping from `@font-face` descriptors into - /// `FontFaceOverrides` is covered by the unit tests in `net.rs`; this - /// test pins the load-side of the pipeline. - #[test] - fn font_face_overrides_alias_family_name() { - const ALIAS: &str = "AliasedFamily"; - - let mut document = BaseDocument::new(DocumentConfig::default()); - - // Sanity: the alias name is not registered before we feed the font. - { - let mut ctx = document.font_ctx.lock().unwrap(); - assert!( - ctx.collection.family_id(ALIAS).is_none(), - "alias must not exist before registration", - ); - } - - // Drive `load_resource` with a `Resource::Font` whose overrides - // assert the CSS-side family name. We use the bullet font as a - // valid font payload — its internal `name` table is irrelevant to - // the assertion; what matters is whether the override wins. - let response = ResourceLoadResponse { - request_id: 0, - node_id: None, - resolved_url: Some(String::from("test://aliased-family")), - result: Ok(Resource::Font( - blitz_traits::net::Bytes::from_static(crate::BULLET_FONT), - FontFaceOverrides { - family_name: Some(String::from(ALIAS)), - weight: Some(800.0), - style: Some(parley::fontique::FontStyle::Italic), - }, - )), - }; - document.load_resource(response); - - // The override must have taken effect: parley's `Collection` now - // resolves the CSS-declared alias to a registered family. - let mut ctx = document.font_ctx.lock().unwrap(); - let family_id = ctx - .collection - .family_id(ALIAS) - .expect("CSS-declared family name should be registered as a family alias"); - let resolved_name = ctx - .collection - .family_name(family_id) - .expect("family id should resolve back to a name"); - assert_eq!( - resolved_name, ALIAS, - "registered family should report the CSS-declared name, \ - not the font file's internal `name` table entry", - ); - } -} diff --git a/packages/blitz-dom/src/events/ime.rs b/packages/blitz-dom/src/events/ime.rs index 6338a2793d..98b3eac48f 100644 --- a/packages/blitz-dom/src/events/ime.rs +++ b/packages/blitz-dom/src/events/ime.rs @@ -14,11 +14,7 @@ pub(crate) fn handle_ime_event( .downcast_element_mut() .and_then(|el| el.text_input_data_mut()); if let Some(input_data) = text_input_data { - let generated_event = input_data.apply_ime_event( - &mut doc.font_ctx.lock().unwrap(), - &mut doc.layout_ctx, - event, - ); + let generated_event = input_data.apply_ime_event(&mut doc.text, event); if let Some(generated_event) = generated_event { doc.apply_generated_text_input_event(node_id, generated_event, dispatch_event); diff --git a/packages/blitz-dom/src/events/keyboard.rs b/packages/blitz-dom/src/events/keyboard.rs index 3ea83557f3..56cee78a90 100644 --- a/packages/blitz-dom/src/events/keyboard.rs +++ b/packages/blitz-dom/src/events/keyboard.rs @@ -1,5 +1,7 @@ use crate::events::focus::generate_focus_events; -use crate::{BaseDocument, node::GeneratedTextInputEvent, util::ACTION_MOD}; +use crate::{ + BaseDocument, node::GeneratedTextInputEvent, text::EditableText as _, util::ACTION_MOD, +}; use blitz_traits::node_id::NodeId; use blitz_traits::{ SmolStr, @@ -78,19 +80,9 @@ pub(crate) fn handle_key_or_input_event( if let Some(input_data) = element_data.text_input_data_mut() { let generated_event = match event { KeyboardOrTextInputEvent::KeyPress(blitz_key_event) => input_data - .apply_keypress_event( - &mut doc.font_ctx.lock().unwrap(), - &mut doc.layout_ctx, - &*doc.shell_provider, - blitz_key_event, - ), + .apply_keypress_event(&mut doc.text, &*doc.shell_provider, blitz_key_event), KeyboardOrTextInputEvent::AppleStandardKeyBinding(command) => input_data - .apply_apple_standard_keybinding( - &mut doc.font_ctx.lock().unwrap(), - &mut doc.layout_ctx, - &*doc.shell_provider, - &command, - ), + .apply_apple_standard_keybinding(&mut doc.text, &*doc.shell_provider, &command), }; if let Some(generated_event) = generated_event { diff --git a/packages/blitz-dom/src/events/pointer.rs b/packages/blitz-dom/src/events/pointer.rs index 4eca266589..b135558835 100644 --- a/packages/blitz-dom/src/events/pointer.rs +++ b/packages/blitz-dom/src/events/pointer.rs @@ -20,6 +20,7 @@ use crate::{ BaseDocument, node::{ScrollbarRef, SpecialElementData}, scrolling::{FlingState, ScrollAnimationState}, + text::{Edit, EditEngine as _, EditableText as _}, }; use super::focus::generate_focus_events; @@ -343,9 +344,9 @@ pub(crate) fn handle_pointermove( y: final_layout.padding.top + final_layout.border.top, }; if !text_input_data.is_multiline { - let layout = text_input_data.editor.try_layout().unwrap(); + let editor = &text_input_data.editor; let content_box_height = final_layout.content_box_height(); - let input_height = layout.height() / layout.scale(); + let input_height = editor.size().unwrap().height as f32 / editor.scale(); let y_offset = ((content_box_height - input_height) / 2.0).max(0.0); content_box_offset.y += y_offset; @@ -363,10 +364,10 @@ pub(crate) fn handle_pointermove( let x = (hit.x - content_box_offset.x) as f64 * doc.viewport.scale_f64() + scroll_x; let y = (hit.y - content_box_offset.y) as f64 * doc.viewport.scale_f64() + scroll_y; - text_input_data - .editor - .driver(&mut doc.font_ctx.lock().unwrap(), &mut doc.layout_ctx) - .extend_selection_to_point(x as f32, y as f32); + text_input_data.editor.edit( + &mut doc.text, + Edit::ExtendSelectionToPoint(x as f32, y as f32), + ); changed = true; } else if event.is_mouse() @@ -462,9 +463,9 @@ pub(crate) fn handle_pointerdown( y: node.final_layout().padding.top + node.final_layout().border.top, }; if !text_input_data.is_multiline { - let layout = text_input_data.editor.try_layout().unwrap(); + let editor = &text_input_data.editor; let content_box_height = node.final_layout().content_box_height(); - let input_height = layout.height() / layout.scale(); + let input_height = editor.size().unwrap().height as f32 / editor.scale(); let y_offset = ((content_box_height - input_height) / 2.0).max(0.0); content_box_offset.y += y_offset; } @@ -521,24 +522,19 @@ pub(crate) fn handle_pointerdown( let node = &mut doc.nodes[actual_target]; let el = node.data.downcast_element_mut().unwrap(); if let SpecialElementData::TextInput(ref mut text_input_data) = el.special_data { - let mut font_ctx = doc.font_ctx.lock().unwrap(); - let mut driver = text_input_data - .editor - .driver(&mut font_ctx, &mut doc.layout_ctx); - - match click_count { + let (tx, ty) = (tx as f32, ty as f32); + let edit = match click_count { 1 => { if mods.shift() { - driver.shift_click_extension(tx as f32, ty as f32); + Edit::ShiftClickExtension(tx, ty) } else { - driver.move_to_point(tx as f32, ty as f32); + Edit::MoveToPoint(tx, ty) } } - 2 => driver.select_word_at_point(tx as f32, ty as f32), - _ => driver.select_hard_line_at_point(tx as f32, ty as f32), - } - - drop(font_ctx); + 2 => Edit::SelectWordAtPoint(tx, ty), + _ => Edit::SelectHardLineAtPoint(tx, ty), + }; + text_input_data.editor.edit(&mut doc.text, edit); } generate_focus_events( diff --git a/packages/blitz-dom/src/font_metrics.rs b/packages/blitz-dom/src/font_metrics.rs deleted file mode 100644 index 5273cdfafc..0000000000 --- a/packages/blitz-dom/src/font_metrics.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use crate::stylo_to_parley; -use app_units::Au; -use parley::{FontContext, FontVariation}; -use skrifa::MetadataProvider as _; -use skrifa::charmap::Charmap; -use style::properties::style_structs::Font as FontStyles; -use style::{ - device::servo::FontMetricsProvider, - font_metrics::FontMetrics, - values::computed::{CSSPixelLength, font::QueryFontMetricsFlags}, -}; - -/// Query fontique for the fonts matching `font_styles` (family list and attributes). -fn query_for_font_styles<'a>( - font_ctx: &'a mut FontContext, - font_styles: &'a FontStyles, -) -> parley::fontique::Query<'a> { - use parley::fontique::Attributes; - - let mut query = font_ctx.collection.query(&mut font_ctx.source_cache); - let families = font_styles - .font_family - .families - .iter() - .map(stylo_to_parley::query_font_family); - query.set_families(families); - query.set_attributes(Attributes { - width: stylo_to_parley::font_width(font_styles.font_width), - weight: stylo_to_parley::font_weight(font_styles.font_weight), - style: stylo_to_parley::font_style(font_styles.font_style), - }); - query -} - -#[derive(Clone)] -pub(crate) struct BlitzFontMetricsProvider { - pub(crate) font_ctx: Arc>, -} - -impl core::fmt::Debug for BlitzFontMetricsProvider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "BlitzFontMetricsProvider") - } -} - -impl FontMetricsProvider for BlitzFontMetricsProvider { - fn query_font_metrics( - &self, - _vertical: bool, - font_styles: &FontStyles, - font_size: CSSPixelLength, - _flags: QueryFontMetricsFlags, - ) -> FontMetrics { - use parley::fontique::{Query, QueryFont, QueryStatus}; - use skrifa::instance::{LocationRef, Size}; - use skrifa::metrics::{GlyphMetrics, Metrics}; - - // Lock font_ctx. Explicit reborrow required for borrow checker. - let mut font_ctx = self.font_ctx.lock().unwrap(); - let font_ctx = &mut *font_ctx; - - // Query fontique for the font that matches the font styles - let mut query = query_for_font_styles(font_ctx, font_styles); - // let fb_script = crate::swash_convert::script_to_fontique(script); - // let fb_language = locale.and_then(crate::swash_convert::locale_to_fontique); - // query.set_fallbacks(fontique::FallbackKey::new(fb_script, fb_language.as_ref())); - - let variations = stylo_to_parley::font_variations(&font_styles.font_variation_settings); - // let features = self.rcx.features(style.font_features).unwrap_or(&[]); - - // fn name_of(font_ref: &skrifa::FontRef) -> String { - // use skrifa::string::StringId; - // font_ref - // .localized_strings(StringId::POSTSCRIPT_NAME) - // .english_or_first() - // .unwrap() - // .chars() - // .collect() - // } - - fn find_font_for(query: &mut Query, ch: char) -> Option { - let mut font = None; - query.matches_with(|q_font: &QueryFont| { - use skrifa::MetadataProvider; - - let Ok(font_ref) = skrifa::FontRef::from_index(q_font.blob.as_ref(), q_font.index) - else { - return QueryStatus::Continue; - }; - - let charmap = font_ref.charmap(); - if charmap.map(ch).is_some() { - font = Some(q_font.clone()); - QueryStatus::Stop - } else { - QueryStatus::Continue - } - }); - font - } - - /// Scales the advance the same way as Parley's shaped glyph advances (font units × size / upem) - /// rather than with skrifa's FreeType-compatible fixed-point scale, so that `ch`/`ic` - /// lengths exactly match the width of the corresponding laid-out text. - fn advance_of( - query: &mut Query, - ch: char, - font_size: f32, - variations: &[FontVariation], - ) -> Option { - let font = find_font_for(query, ch)?; - let font_ref = skrifa::FontRef::from_index(font.blob.as_ref(), font.index).ok()?; - let location = font_ref.axes().location( - variations - .iter() - .map(|v| (skrifa::Tag::from_be_bytes(v.tag.to_bytes()), v.value)), - ); - let location_ref = LocationRef::from(&location); - let upem = Metrics::new(&font_ref, Size::unscaled(), location_ref).units_per_em; - if upem == 0 { - return None; - } - let glyph_metrics = GlyphMetrics::new(&font_ref, Size::unscaled(), location_ref); - let char_map = Charmap::new(&font_ref); - let glyph_id = char_map.map(ch)?; - let advance = glyph_metrics.advance_width(glyph_id)?; - Some(advance * (font_size / upem as f32)) - } - - fn metrics_of( - query: &mut Query, - ch: char, - font_size: Size, - variations: &[FontVariation], - ) -> Option<(f32, Option, Option)> { - let font = find_font_for(query, ch)?; - let font_ref = skrifa::FontRef::from_index(font.blob.as_ref(), font.index).ok()?; - let location = font_ref.axes().location( - variations - .iter() - .map(|v| (skrifa::Tag::from_be_bytes(v.tag.to_bytes()), v.value)), - ); - let location_ref = LocationRef::from(&location); - let metrics = Metrics::new(&font_ref, font_size, location_ref); - Some((metrics.ascent, metrics.x_height, metrics.cap_height)) - } - - let zero_advance = advance_of(&mut query, '0', font_size.px(), &variations); - let ic_advance = advance_of(&mut query, '\u{6C34}', font_size.px(), &variations); - let (ascent, x_height, cap_height) = - metrics_of(&mut query, ' ', Size::new(font_size.px()), &variations) - .unwrap_or((0.0, None, None)); - - FontMetrics { - ascent: CSSPixelLength::new(ascent), - x_height: x_height.filter(|xh| *xh != 0.0).map(CSSPixelLength::new), - cap_height: cap_height.map(CSSPixelLength::new), - zero_advance_measure: zero_advance.map(CSSPixelLength::new), - ic_width: ic_advance.map(CSSPixelLength::new), - script_percent_scale_down: None, - script_script_percent_scale_down: None, - } - } - - fn base_size_for_generic( - &self, - generic: style::values::computed::font::GenericFontFamily, - ) -> style::values::computed::Length { - let size = match generic { - style::values::computed::font::GenericFontFamily::Monospace => 13.0, - _ => 16.0, - }; - style::values::computed::Length::from(Au::from_f32_px(size)) - } -} diff --git a/packages/blitz-dom/src/form.rs b/packages/blitz-dom/src/form.rs index 86f5a66f48..1c4ea1ceb4 100644 --- a/packages/blitz-dom/src/form.rs +++ b/packages/blitz-dom/src/form.rs @@ -3,6 +3,7 @@ use markup5ever::{LocalName, local_name}; use crate::{ BaseDocument, ElementData, + text::EditableText as _, traversal::{AncestorTraverser, TreeTraverser}, }; use blitz_traits::{ @@ -314,7 +315,7 @@ fn construct_entry_list(doc: &BaseDocument, form_id: NodeId, submitter_id: NodeI } // Otherwise, create an entry with name and the value of the field element, and append it to entry list. else if let Some(text) = element.text_input_data() { - create_entry(name, text.editor.text().to_string().as_str().into()); + create_entry(name, text.editor.text().as_str().into()); } else if let Some(value) = element.attr(local_name!("value")) { create_entry(name, value.into()); } diff --git a/packages/blitz-dom/src/iframe.rs b/packages/blitz-dom/src/iframe.rs index 801366d9a9..2c65166a70 100644 --- a/packages/blitz-dom/src/iframe.rs +++ b/packages/blitz-dom/src/iframe.rs @@ -10,6 +10,7 @@ use blitz_traits::shell::ShellProvider; use crate::document::DocumentEvent; use crate::net::{DocumentSrcHandler, ResourceHandler, stamped_request}; +use crate::text::DocumentText as _; use crate::{BaseDocument, DocumentConfig, local_name}; /// Maximum nesting depth of documents-within-documents. Iframes nested deeper @@ -66,7 +67,7 @@ impl BaseDocument { })), shell_provider: Some(self.shell_provider.clone()), html_parser_provider: Some(self.html_parser_provider.clone()), - font_ctx: Some(self.font_ctx.lock().unwrap().clone()), + font_ctx: Some(self.text.fonts()), media_type: Some(self.media_type.clone()), style_threading: self.style_threading, incremental: Some(self.incremental_layout), diff --git a/packages/blitz-dom/src/layout/construct.rs b/packages/blitz-dom/src/layout/construct.rs index a1b07ff6f6..4f56cb152e 100644 --- a/packages/blitz-dom/src/layout/construct.rs +++ b/packages/blitz-dom/src/layout/construct.rs @@ -3,10 +3,6 @@ use core::str; use std::sync::Arc; use markup5ever::{QualName, local_name, ns}; -use parley::{ - FontContext, InlineBox, InlineBoxKind, LayoutContext, StyleProperty, TreeBuilder, - WhiteSpaceCollapse, -}; use style::{ computed_values::position::T as PositionProperty, data::ElementData as StyloElementData, @@ -23,20 +19,15 @@ use crate::{ layout::damage::{ CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC, sort_layout_children_by_order, }, - node::{ - ListItemLayout, ListItemLayoutPosition, Marker, NodeFlags, NodeKind, SpecialElementData, - TextBrush, TextInputData, TextLayout, - }, - qual_name, stylo_to_parley, + node::{NodeFlags, NodeKind, SpecialElementData, TextInputData, TextLayout}, + qual_name, + text::EditEngine as _, traversal::{iter_children, iter_children_and_pseudos}, }; use super::{ - damage::ALL_DAMAGE, - list::{BULLET_FONT_FAMILY, collect_list_item_children}, - replaced::is_inline_box_element, + damage::ALL_DAMAGE, list::collect_list_item_children, replaced::is_inline_box_element, table::build_table_context, - text_transform::{CaseTransform, TextTransformer}, }; const DUMMY_NAME: QualName = qual_name!("div", html); @@ -47,20 +38,11 @@ pub(crate) struct ConstructionTask { pub(crate) data: ConstructionTaskData, } -pub(crate) struct ConstructionTaskResult { - pub(crate) node_id: NodeId, - pub(crate) data: ConstructionTaskResultData, -} - #[derive(Clone)] pub(crate) enum ConstructionTaskData { InlineLayout(Box), } -pub(crate) enum ConstructionTaskResultData { - InlineLayout(Box), -} - /// Accumulator threaded through layout-child collection. /// /// `children` is the list of layout children being built up, and @@ -613,7 +595,7 @@ fn collect_layout_children_with_wrap( let existing_layout = doc.nodes[container_node_id] .element_data_mut() .and_then(|el| el.inline_layout_data.take()); - let layout = existing_layout.unwrap_or_else(|| Box::new(TextLayout::new())); + let layout = existing_layout.unwrap_or_default(); // Queue node for inline layout construction. Deferring construction of inline layouts to a // dedicated phase allows us to multithread the expensive text shaping step. @@ -854,19 +836,7 @@ fn collect_complex_layout_children( fn create_text_editor(doc: &mut BaseDocument, input_element_id: NodeId, is_multiline: bool) { let node = &mut doc.nodes[input_element_id]; - let parley_style = node - .primary_styles() - .as_ref() - .map(|s| stylo_to_parley::style(node.id, s)) - .unwrap_or_default(); - let alignment = node - .primary_styles() - .map(|s| stylo_to_parley::text_align(s.clone_text_align())) - .unwrap_or(parley::layout::Alignment::Start); - let base_direction = node - .primary_styles() - .map(|s| stylo_to_parley::base_direction(s.clone_direction(), s.clone_unicode_bidi())) - .unwrap_or(parley::BaseDirection::Auto); + let style = node.primary_styles().map(|s| (*s).clone()); let initial_text = if is_multiline { node.text_content() @@ -876,8 +846,7 @@ fn create_text_editor(doc: &mut BaseDocument, input_element_id: NodeId, is_multi let element = &mut node.data.downcast_element_mut().unwrap(); if !matches!(element.special_data, SpecialElementData::TextInput(_)) { let mut text_input_data = TextInputData::new(is_multiline); - let editor = &mut text_input_data.editor; - editor.set_text(&initial_text); + text_input_data.editor.set_initial_text(&initial_text); element.special_data = SpecialElementData::TextInput(text_input_data); } @@ -885,33 +854,12 @@ fn create_text_editor(doc: &mut BaseDocument, input_element_id: NodeId, is_multi unreachable!(); }; - let editor = &mut text_input_data.editor; - editor.set_scale(doc.viewport.scale_f64() as f32); - editor.set_width(None); - - let styles = editor.edit_styles(); - styles.retain(|_| false); - styles.insert(StyleProperty::FontFamily(parley_style.font_family)); - styles.insert(StyleProperty::FontSize(parley_style.font_size)); - styles.insert(StyleProperty::FontWidth(parley_style.font_width)); - styles.insert(StyleProperty::FontStyle(parley_style.font_style)); - styles.insert(StyleProperty::FontWeight(parley_style.font_weight)); - styles.insert(StyleProperty::FontVariations(parley_style.font_variations)); - styles.insert(StyleProperty::FontFeatures(parley_style.font_features)); - styles.insert(StyleProperty::LineHeight(parley_style.line_height)); - styles.insert(StyleProperty::WordSpacing(parley_style.word_spacing)); - styles.insert(StyleProperty::LetterSpacing(parley_style.letter_spacing)); - styles.insert(StyleProperty::WordBreak(parley_style.word_break)); - styles.insert(StyleProperty::OverflowWrap(parley_style.overflow_wrap)); - styles.insert(StyleProperty::TextWrapMode(parley_style.text_wrap_mode)); - styles.insert(StyleProperty::WhiteSpaceCollapse( - parley_style.white_space_collapse, - )); - styles.insert(StyleProperty::Brush(parley_style.brush)); - editor.set_alignment(alignment); - editor.set_base_direction(base_direction); - - editor.refresh_layout(&mut doc.font_ctx.lock().unwrap(), &mut doc.layout_ctx); + text_input_data.editor.set_style( + &mut doc.text, + input_element_id, + style.as_deref(), + doc.viewport.scale_f64() as f32, + ); } fn create_checkbox_input(doc: &mut BaseDocument, input_element_id: NodeId) { @@ -1030,285 +978,3 @@ pub(crate) fn find_inline_layout_embedded_boxes( } } } - -pub(crate) fn build_inline_layout_into( - nodes: &crate::NodeTree, - layout_ctx: &mut LayoutContext, - font_ctx: &mut FontContext, - text_layout: &mut TextLayout, - scale: f32, - inline_context_root_node_id: NodeId, -) { - // Get the inline context's root node's text styles - let root_node = &nodes[inline_context_root_node_id]; - let root_node_style = root_node.primary_styles().or_else(|| { - root_node - .parent - .and_then(|parent_id| nodes[parent_id].primary_styles()) - }); - - let parley_style = root_node_style - .as_ref() - .map(|s| stylo_to_parley::style(inline_context_root_node_id, s)) - .unwrap_or_else(|| parley::TextStyle { - white_space_collapse: WhiteSpaceCollapse::Collapse, - ..Default::default() - }); - - // Create a parley tree builder - let mut builder = layout_ctx.tree_builder(font_ctx, scale, true, &parley_style); - if let Some(style) = root_node_style.as_deref() { - builder.set_base_direction(stylo_to_parley::base_direction( - style.clone_direction(), - style.clone_unicode_bidi(), - )); - } - - let text_transform = root_node_style - .as_deref() - .map(|s| CaseTransform::from_style(s)) - .unwrap_or(CaseTransform::NONE); - - // Render position-inside list items - if let Some(ListItemLayout { - marker, - position: ListItemLayoutPosition::Inside, - }) = root_node - .element_data() - .and_then(|el| el.list_item_data.as_deref()) - { - match marker { - // Bullet glyphs live in the bundled bullet font. The position-outside - // path already asks for it; without the same span here a marker like - // disclosure-closed (U+25B8) falls back to the element's own font and - // renders as a missing glyph. - Marker::Char(char) => { - let mut marker_style = parley_style.clone(); - marker_style.font_family = BULLET_FONT_FAMILY.into(); - builder.push_style_span(marker_style); - builder.push_text(&format!("{char} ")); - builder.pop_style_span(); - } - Marker::String(str) => builder.push_text(str), - } - }; - // The marker is a separate box, so words in the content don't continue from it. - let mut text_transformer = TextTransformer::default(); - text_transformer.word_break(&builder); - - if let Some(before_id) = root_node.before() { - build_inline_layout_recursive( - &mut builder, - &mut text_transformer, - nodes, - before_id, - &text_transform, - ); - } - for child_id in root_node.children.iter().copied() { - build_inline_layout_recursive( - &mut builder, - &mut text_transformer, - nodes, - child_id, - &text_transform, - ); - } - if let Some(after_id) = root_node.after() { - build_inline_layout_recursive( - &mut builder, - &mut text_transformer, - nodes, - after_id, - &text_transform, - ); - } - - text_layout.text = builder.build_into(&mut text_layout.layout); - return; - - fn build_inline_layout_recursive( - builder: &mut TreeBuilder, - text_transformer: &mut TextTransformer, - nodes: &crate::NodeTree, - node_id: NodeId, - parent_text_transform: &CaseTransform, - ) { - let node = &nodes[node_id]; - - let style = node.primary_styles(); - let style = style.as_ref(); - - let text_transform = style - .map(|s| CaseTransform::from_style(s)) - .unwrap_or(CaseTransform::NONE); - - match &node.data { - NodeData::Element(element_data) | NodeData::AnonymousBlock(element_data) => { - // if the input type is hidden, hide it - if *element_data.name.local == *"input" { - if let Some("hidden") = element_data.attr(local_name!("type")) { - return; - } - } - - let display = node.display_style().unwrap_or(Display::inline()); - let position = style - .map(|s| s.clone_position()) - .unwrap_or(PositionProperty::Static); - let float = style.map(|s| s.clone_float()).unwrap_or(Float::None); - let box_kind = if position.is_absolutely_positioned() { - InlineBoxKind::OutOfFlow - } else if float.is_floating() { - InlineBoxKind::CustomOutOfFlow - } else { - InlineBoxKind::InFlow - }; - - match (display.outside(), display.inside()) { - (DisplayOutside::None, DisplayInside::None) => { - // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); - } - (DisplayOutside::None, DisplayInside::Contents) => { - let whitespace = style.map(|s| { - [ - parley::StyleProperty::WhiteSpaceCollapse( - stylo_to_parley::white_space_collapse( - s.clone_white_space_collapse(), - ), - ), - parley::StyleProperty::TextWrapMode( - stylo_to_parley::text_wrap_mode(s.clone_text_wrap_mode()), - ), - ] - }); - builder.push_style_modification_span( - whitespace - .as_ref() - .map_or(&[][..], |styles| styles.as_slice()), - ); - for child_id in node.children.iter().copied() { - // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); - build_inline_layout_recursive( - builder, - text_transformer, - nodes, - child_id, - &text_transform, - ); - } - builder.pop_style_span(); - } - (DisplayOutside::Inline, DisplayInside::Flow) => { - let tag_name = &element_data.name.local; - - if is_inline_box_element(tag_name) { - if box_kind == InlineBoxKind::InFlow { - text_transformer.word_break(builder); - } - builder.push_inline_box(InlineBox { - id: node_id.as_u64(), - kind: box_kind, - // Overridden by push_inline_box method - index: 0, - // Width and height are set during layout - width: 0.0, - height: 0.0, - baseline: None, - vertical_align: node - .primary_styles() - .map(|s| stylo_to_parley::vertical_align(&s)) - .unwrap_or_default(), - }); - } else if *tag_name == local_name!("br") { - // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); - // TODO: update span id for br spans - builder.push_style_modification_span(&[ - parley::StyleProperty::WhiteSpaceCollapse( - WhiteSpaceCollapse::Preserve, - ), - ]); - builder.push_text("\n"); - builder.pop_style_span(); - text_transformer.word_break(builder); - } else { - // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); - let style = node - .primary_styles() - .map(|s| stylo_to_parley::style(node.id, &s)) - .unwrap_or_else(|| parley::TextStyle { - white_space_collapse: WhiteSpaceCollapse::Collapse, - ..Default::default() - }); - - builder.push_style_span(style); - - if let Some(before_id) = node.before() { - build_inline_layout_recursive( - builder, - text_transformer, - nodes, - before_id, - &text_transform, - ); - } - - for child_id in node.children.iter().copied() { - build_inline_layout_recursive( - builder, - text_transformer, - nodes, - child_id, - &text_transform, - ); - } - if let Some(after_id) = node.after() { - build_inline_layout_recursive( - builder, - text_transformer, - nodes, - after_id, - &text_transform, - ); - } - - builder.pop_style_span(); - } - } - // Inline box - (_, _) => { - if box_kind == InlineBoxKind::InFlow { - text_transformer.word_break(builder); - } - builder.push_inline_box(InlineBox { - id: node_id.as_u64(), - kind: box_kind, - // Overridden by push_inline_box method - index: 0, - // Width and height are set during layout - width: 0.0, - height: 0.0, - baseline: None, - vertical_align: node - .primary_styles() - .map(|s| stylo_to_parley::vertical_align(&s)) - .unwrap_or_default(), - }); - } - }; - } - NodeData::Text(data) => { - // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); - // dbg!(&data.content); - - let text = - text_transformer.transform(&data.content, parent_text_transform, builder); - builder.push_text(text); - } - NodeData::Comment { .. } => { - // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); - } - NodeData::Document(_) => unreachable!(), - } - } -} diff --git a/packages/blitz-dom/src/layout/damage.rs b/packages/blitz-dom/src/layout/damage.rs index aaa5f5e26e..fea1870922 100644 --- a/packages/blitz-dom/src/layout/damage.rs +++ b/packages/blitz-dom/src/layout/damage.rs @@ -4,6 +4,7 @@ use markup5ever::local_name; use crate::net::ResourceHandler; use crate::node::{Node, NodeFlags}; +use crate::text::EditEngine as _; use crate::tree::NodeTree; use crate::{ BaseDocument, net::ImageHandler, node::ImageResourceData, node::Status, util::ImageLayerKind, @@ -482,8 +483,7 @@ impl BaseDocument { pub(crate) fn invalidate_inline_contexts(&mut self) { let scale = self.viewport.scale(); - let font_ctx = &self.font_ctx; - let layout_ctx = &mut self.layout_ctx; + let text = &mut self.text; let mut anon_nodes = Vec::new(); @@ -503,9 +503,7 @@ impl BaseDocument { node.insert_damage(ALL_DAMAGE); } } else if let Some(input) = element.text_input_data_mut() { - input.editor.set_scale(scale); - let mut font_ctx = font_ctx.lock().unwrap(); - input.editor.refresh_layout(&mut font_ctx, layout_ctx); + input.editor.set_scale(text, scale); node.insert_damage(ONLY_RELAYOUT); } } diff --git a/packages/blitz-dom/src/layout/inline.rs b/packages/blitz-dom/src/layout/inline.rs index e5d0118ad5..c53ec1658b 100644 --- a/packages/blitz-dom/src/layout/inline.rs +++ b/packages/blitz-dom/src/layout/inline.rs @@ -1,31 +1,41 @@ use blitz_traits::node_id::NodeId; -use parley::{AlignmentOptions, BreakReason, IndentOptions}; -use style::values::specified::box_::{DisplayInside, DisplayOutside}; -use style::values::{ - computed::{CSSPixelLength, Contain}, - generics::text::GenericTextIndent, -}; use taffy::{ - AvailableSpace, AxisStaticPosition, BlockContainerStyle, BlockContext, BlockFormattingContext, - BoxSizing, CollapsibleMarginSet, CompactLength, CoreStyle as _, Direction, LayoutInput, - LayoutOutput, LayoutPartialTree as _, MaybeMath as _, MaybeResolve as _, OofCandidate, - OofCandidates, OofItemStyle, OofPositioningArea, Overflow, Point, RequestedAxis, + AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, CompactLength, CoreStyle as _, + LayoutInput, LayoutOutput, MaybeMath as _, MaybeResolve as _, Overflow, Point, Rect, ResolveOrZero as _, RunMode, Size, SizingMode, }; -#[cfg(feature = "floats")] -use parley::YieldData; -#[cfg(feature = "floats")] -use taffy::{BlockItemStyle as _, Clear, Float, prelude::TaffyMaxContent}; - use super::resolve_calc_value; use crate::layout::LayoutPassState; -use crate::stylo_to_parley; +use crate::node::TextLayout; +use crate::text::InlineLayoutEngine as _; + +/// What `compute_inline_layout_inner` has resolved from the container's styles and inputs +/// before the text backend measures the inline boxes and breaks lines. +pub(crate) struct Frame { + pub(crate) inputs: LayoutInput, + pub(crate) node_size: Size>, + pub(crate) node_min_size: Size>, + pub(crate) node_max_size: Size>, + pub(crate) aspect_ratio: Option, + pub(crate) padding: Rect, + pub(crate) border: Rect, + pub(crate) scrollbar_gutter: Point, + pub(crate) container_pb: Rect, + pub(crate) content_box_inset: Rect, + pub(crate) child_inputs: LayoutInput, + pub(crate) available_space: Size, + pub(crate) collapses_through: bool, + pub(crate) scale: f32, +} /// Subtract a child's margins from the definite axes of the available space it is laid out in. /// Taffy's convention is that the parent subtracts a child's margins from the available space /// before passing it to the child, and that the child does not subtract them again. -fn subtract_margins(mut child_inputs: LayoutInput, margin: taffy::Rect) -> LayoutInput { +pub(crate) fn subtract_margins( + mut child_inputs: LayoutInput, + margin: taffy::Rect, +) -> LayoutInput { child_inputs.available_space = child_inputs .available_space .maybe_sub(margin.sum_axes()) @@ -36,7 +46,7 @@ fn subtract_margins(mut child_inputs: LayoutInput, margin: taffy::Rect) -> /// Layout inputs for an atomic inline box, with any sizing keyword on its `width` style /// (`min-content`, `max-content`, `fit-content`, `fit-content(...)`, `stretch`) resolved /// into the available space or known width the box is measured with. -fn inline_box_inputs( +pub(crate) fn inline_box_inputs( width_style: taffy::Dimension, margin: taffy::Rect, child_inputs: LayoutInput, @@ -208,7 +218,7 @@ impl LayoutPassState<'_> { } = inputs; // Take inline layout to satisfy borrow checker - let mut inline_layout = self.nodes[node_id] + let inline_layout = self.nodes[node_id] .data .downcast_element_mut() .unwrap() @@ -292,22 +302,6 @@ impl LayoutPassState<'_> { drop(style); - // Short circuit if inline context contains no text or inline boxes - if !has_styles_preventing_being_collapsed_through - && inline_layout.text.is_empty() - && inline_layout.layout.inline_boxes().len() == 0 - { - // Put layout back - self.nodes[node_id] - .data - .downcast_element_mut() - .unwrap() - .inline_layout_data = Some(inline_layout); - return LayoutOutput::from_outer_size( - Size::ZERO.maybe_max(container_pb.sum_axes().map(Some)), - ); - } - // Compute available space let available_space = Size { width: known_dimensions @@ -353,762 +347,27 @@ impl LayoutPassState<'_> { vertical_margins_are_collapsible: taffy::Line::FALSE, ..inputs }; - #[cfg(feature = "floats")] - let float_child_inputs = taffy::tree::LayoutInput { - available_space: Size::MAX_CONTENT, - ..child_inputs - }; - - // Update inline boxes - for ibox in inline_layout.layout.inline_boxes_mut() { - let style = self.child_layout_style(&self.nodes[NodeId::from_u64(ibox.id)]); - let margin = style - .margin() - .resolve_or_zero(inputs.parent_size, resolve_calc_value); - - #[cfg(feature = "floats")] - let is_floated = style.float().is_floated(); - #[cfg(not(feature = "floats"))] - let is_floated = false; - - let is_out_of_flow = style.position().is_out_of_flow(); - // The baseline of an inline-block is the baseline of its last in-flow line box, - // unless it has no line boxes or it is a block-axis scroll container, in which - // case it is the bottom margin edge (CSS 2 §10.8.1, css-align-3 §9.1 - // `baseline-source: auto`; `overflow: clip` is not a scroll container). Other - // atomic inlines (flex, grid, table) export a baseline regardless of `overflow`, - // clamped to their border box if they are scroll containers (css-align-3 §9.1). - // A layout-contained box is treated as having no baseline (css-contain-1 §3.3). - let overflow = style.overflow(); - let box_style = style.style.get_box(); - let is_flow = matches!( - box_style.display.inside(), - DisplayInside::Flow | DisplayInside::FlowRoot - ); - let is_scroll_container = !matches!(overflow.y, Overflow::Visible | Overflow::Clip); - let is_block_axis_scroll_container = is_flow && is_scroll_container; - let contain_layout = box_style.clone_contain().contains(Contain::LAYOUT); - let exports_baseline = !is_block_axis_scroll_container && !contain_layout; - let box_inputs = inline_box_inputs(style.size().width, margin, child_inputs); - drop(style); - - if is_out_of_flow || is_floated { - ibox.width = 0.0; - ibox.height = 0.0; - ibox.baseline = None; - } else { - let output = self.compute_child_layout(taffy::NodeId::from(ibox.id), box_inputs); - ibox.width = (margin.left + margin.right + output.size.width) * scale; - ibox.baseline = if exports_baseline { - output - .baselines - .last - .or(output.baselines.first) - .map(|baseline| { - let baseline = if is_scroll_container { - baseline.clamp(0.0, output.size.height) - } else { - baseline - }; - (margin.top + baseline) * scale - }) - } else { - None - }; - // Vertical margins adjust the space the box reserves in the line. A box with a - // baseline splits that space into ascent (`margin.top + baseline`) and descent - // (`margin.bottom + height - baseline`), either of which may be negative. A box - // without a baseline sits on the baseline and cannot reserve negative space. - // Kept finite: huge author lengths can sum to infinity, which is taller than - // the line breaker's `f32::MAX` height limit, and it then yields - // `MaxHeightExceeded` for this box forever without advancing. - let margin_box_height = margin.top + margin.bottom + output.size.height; - ibox.height = if ibox.baseline.is_some() { - (margin_box_height * scale).min(f32::MAX) - } else { - (margin_box_height.max(0.0) * scale).min(f32::MAX) - }; - } - } - - let text_indent = self.nodes[node_id] - .primary_styles() - .map(|s| s.clone_text_indent()) - .unwrap_or_else(GenericTextIndent::zero); - let indent_options = IndentOptions { - each_line: text_indent.each_line, - hanging: text_indent.hanging, - }; - // Percentage indents do not contribute to intrinsic widths. - inline_layout.layout.set_text_indent( - text_indent.length.resolve(CSSPixelLength::new(0.0)).px() * scale, - indent_options, - ); - - let pbw = container_pb.horizontal_components().sum() * scale; - let width = known_dimensions - .width - .map(|w| (w * scale) - pbw) - .unwrap_or_else(|| { - // TODO: Cache content widths. - // - // This is a little tricky as the size of the inline boxes may depend on whether we are sizing under - // and a min-content or max-content constraint. So if we want to compute both widths in one pass then - // we need to store both a min-content and max-content size on each box. - let content_sizes = inline_layout.layout.calculate_content_widths(); - let min_content_width = content_sizes.min; - let max_content_width = content_sizes.max; - - #[cfg(feature = "floats")] - let float_width = match available_space.width { - AvailableSpace::Definite(_) => 0.0, - AvailableSpace::MinContent => { - let mut width: f32 = 0.0; - for ibox in inline_layout.layout.inline_boxes_mut() { - let (is_floated, margin) = { - let style = - self.child_layout_style(&self.nodes[NodeId::from_u64(ibox.id)]); - ( - style.float().is_floated(), - style - .margin() - .resolve_or_zero(inputs.parent_size, resolve_calc_value), - ) - }; - - if is_floated { - let output = self.compute_child_layout( - taffy::NodeId::from(ibox.id), - subtract_margins(child_inputs, margin), - ); - width = width.max(output.size.width + margin.left + margin.right); - } - } - - width * scale - } - AvailableSpace::MaxContent => { - // When computing a max-content size the available width is effectively - // infinite, so floats never wrap onto a new "band" due to a lack of - // horizontal space. They only move below preceding floats when the `clear` - // property forces them to. - // - // Floats that share a band sit side-by-side and so their widths sum, whereas - // floats pushed onto a new band (via `clear`) stack vertically and so we - // take the maximum extent across bands rather than summing. - let mut left_band: f32 = 0.0; - let mut right_band: f32 = 0.0; - let mut width: f32 = 0.0; - for ibox in inline_layout.layout.inline_boxes_mut() { - let (float, clear, margin) = { - let style = - self.child_layout_style(&self.nodes[NodeId::from_u64(ibox.id)]); - ( - style.float(), - style.clear(), - style - .margin() - .resolve_or_zero(inputs.parent_size, resolve_calc_value), - ) - }; - - if float.is_floated() { - if matches!(clear, Clear::Left | Clear::Both) { - left_band = 0.0; - } - if matches!(clear, Clear::Right | Clear::Both) { - right_band = 0.0; - } - - let output = self.compute_child_layout( - taffy::NodeId::from(ibox.id), - subtract_margins(child_inputs, margin), - ); - let box_width = output.size.width + margin.left + margin.right; - - match float { - Float::Left => left_band += box_width, - Float::Right => right_band += box_width, - Float::None => {} - } - width = width.max(left_band + right_band); - } - } - - width * scale - } - }; - - #[cfg(not(feature = "floats"))] - let float_width = 0.0; - - let computed_width = match available_space.width { - AvailableSpace::MinContent => min_content_width.max(float_width), - AvailableSpace::MaxContent => max_content_width + float_width, - AvailableSpace::Definite(limit) => (limit * scale) - .min(max_content_width + float_width) - .max(min_content_width), - } - .ceil(); - - let style_width = node_size.width.map(|w| w * scale); - let min_width = node_min_size.width.map(|w| w * scale); - let max_width = node_max_size.width.map(|w| w * scale); - - (style_width) - .unwrap_or(computed_width + pbw) - .max(computed_width) - .maybe_clamp(min_width, max_width) - - pbw - }); - - #[cfg(not(feature = "floats"))] - let _ = block_ctx; // Suppress unused variable warning - - // Set block context width if this is a block context root - #[cfg(feature = "floats")] - let is_bfc_root = block_ctx.is_bfc_root(); - #[cfg(feature = "floats")] - if is_bfc_root { - block_ctx.set_width((width + pbw) / scale); - } - - // Create sub-context to account for the inline layout's padding/border - #[cfg(feature = "floats")] - let outer_block_ctx = block_ctx; - #[cfg(feature = "floats")] - let mut block_ctx = - outer_block_ctx.sub_context(container_pb.top, [container_pb.left, container_pb.right]); - // block_ctx.apply_content_box_inset([container_pb.left, container_pb.right]); - - if inputs.run_mode == taffy::RunMode::ComputeSize - && inputs.axis == RequestedAxis::Horizontal - { - // Put layout back - self.nodes[node_id] - .data - .downcast_element_mut() - .unwrap() - .inline_layout_data = Some(inline_layout); - - let measured_size = inputs.known_dimensions.unwrap_or(taffy::Size { - width: width.ceil() / scale, - // Height is ignored if RequestedAxis if Horizontal - height: 0.0, - }); - - let clamped_size = inputs - .known_dimensions - .or(node_size) - .unwrap_or(measured_size + content_box_inset.sum_axes()) - .maybe_clamp(node_min_size, node_max_size) - .maybe_max(container_pb.sum_axes().map(Some)); - - return LayoutOutput::from_outer_size(clamped_size); - } - - let resolved_text_indent = text_indent - .length - .resolve(CSSPixelLength::new((width / scale).max(0.0))) - .px(); - inline_layout - .layout - .set_text_indent(resolved_text_indent * scale, indent_options); - - #[cfg(not(feature = "floats"))] - { - inline_layout.layout.break_all_lines(Some(width)); - } - - // Out-of-flow candidates bubbled up from this container and its in-flow subtree. - // These are laid out by the out-of-flow positioning pass (`compute_oof_layout`). - let mut oof_candidates = OofCandidates::new(); - - // Perform inline layout - #[cfg(feature = "floats")] - { - let mut breaker = inline_layout.layout.break_lines(); - let initial_slot = block_ctx.find_content_slot(0.0, Clear::None, None); - let mut has_active_floats = initial_slot.segment_id.is_some(); - let state = breaker.state_mut(); - state.set_layout_max_advance(width); - state.set_line_max_advance(initial_slot.width * scale); - state.set_line_x(initial_slot.x * scale); - state.set_line_y((initial_slot.y * scale) as f64); - - // TODO: revert state and retry layout if a line doesn't fit - // - // Save initial state. Saved state is used to revert the layout to a previous state if needed - // (e.g. to revert a line that doesn't fit in the space it was laid out into) - // - // let mut saved_state = breaker.state().clone(); - - while let Some(yield_data) = breaker.break_next() { - match yield_data { - YieldData::LineBreak(_line_break_data) => { - let state = breaker.state_mut(); - - if has_active_floats { - // TODO: revert state and retry layout if a line doesn't fit - // saved_state = state.clone(); - - let min_y = state.line_y() / scale as f64; - let next_slot = - block_ctx.find_content_slot(min_y as f32, Clear::None, None); - has_active_floats = next_slot.segment_id.is_some(); - - state.set_line_max_advance(next_slot.width * scale); - state.set_line_x(next_slot.x * scale); - state.set_line_y((next_slot.y * scale) as f64); - } else { - state.set_line_x(0.0); - state.set_line_max_advance(width); - } - - continue; - } - YieldData::MaxHeightExceeded(_data) => { - // TODO - continue; - } - YieldData::InlineBoxBreak(box_break_data) => { - let state = breaker.state_mut(); - let node_id = NodeId::from_u64(box_break_data.inline_box_id); - - let (direction, clear, margin) = { - let style = self.child_layout_style(&self.nodes[node_id]); - // We can assume that the box is a float because we only set `break_on_box: true` for floats - let direction = match style.float() { - Float::Left => taffy::FloatDirection::Left, - Float::Right => taffy::FloatDirection::Right, - Float::None => unreachable!(), - }; - ( - direction, - style.clear(), - style - .margin() - .resolve_or_zero(inputs.parent_size, resolve_calc_value), - ) - }; - - let margin_sum = margin.sum_axes(); - - let mut output = self.compute_child_layout( - crate::taffy_node_id(node_id), - float_child_inputs, - ); - let min_y = state.line_y() as f32 / scale; - - // Note: `pos` is content-box relative - let pos = block_ctx.place_floated_box( - output.size + margin_sum, - min_y, - direction, - clear, - false, - ); - - let min_y = state.line_y() / scale as f64; //.max(pos.y as f64); - let next_slot = - block_ctx.find_content_slot(min_y as f32, Clear::None, None); - has_active_floats = next_slot.segment_id.is_some(); - - state.set_line_max_advance(next_slot.width * scale); - state.set_line_x(next_slot.x * scale); - state.set_line_y((next_slot.y * scale) as f64); - - let location = taffy::Point { - x: pos.x + margin.left + container_pb.left, - y: pos.y + margin.top + container_pb.top, - }; - let layout = self.nodes[node_id].unrounded_layout_mut(); - layout.size = output.size; - layout.location = location; - layout.margin = margin; - - // Translate anchors from item-relative to container-relative - // coordinates and collect candidates bubbled from the float's subtree - if !output.oof_candidates.is_empty() { - output.oof_candidates.translate(location); - oof_candidates.append(&mut output.oof_candidates); - } - - // dbg!(&layout.size); - // dbg!(&layout.location); - - // Floats are out-of-flow and must not contribute to the line's height. - state.append_inline_box_to_line( - box_break_data.advance, - f32::NEG_INFINITY, - f32::NEG_INFINITY, - ); - - // if float.is_floated() { - // println!("INLINE FLOATED BOX ({}) {:?}", ibox.id, float); - // println!( - // "w:{} h:{} x:{}, y:{}", - // layout.size.width, layout.size.height, 0, 0 - // ); - // } - } - } - } - breaker.finish(); - } - - // Propagate the height consumed by floats placed within this container to the - // enclosing block context (so that the BFC root can contain them) - #[cfg(feature = "floats")] - let float_height_contribution = block_ctx.floated_content_height_contribution(); - #[cfg(feature = "floats")] - outer_block_ctx.add_child_floated_content_height_contribution( - container_pb.top + float_height_contribution, - ); - - let (alignment, last_line_alignment) = self.nodes[node_id] - .primary_styles() - .map(|s| { - ( - stylo_to_parley::text_align(s.clone_text_align()), - stylo_to_parley::text_align_last(s.clone_text_align_last()), - ) - }) - .unwrap_or((parley::layout::Alignment::Start, None)); - - inline_layout.layout.align( - alignment, - AlignmentOptions { - align_when_overflowing: false, - last_line_alignment, - }, - ); - - // Parley lays out empty text as a single strut-height line (text-editor semantics), - // but a line box containing no text, inline boxes or other in-flow content is a - // zero-height line box in CSS (CSS2 §9.4.2). - let has_inline_content = - !inline_layout.text.is_empty() || inline_layout.layout.inline_boxes().len() > 0; - - let mut height = if has_inline_content { - inline_layout.layout.height() - } else { - 0.0 - }; - - // A forced line break (e.g. `
` or a preserved newline) at the end of the inline - // content ends the final line box without starting a new one. Parley still emits an - // empty line after it (so that editors have a line to place the cursor on), so that - // line is excluded from the measured height (and from the last baseline). - let line_count = inline_layout.layout.len(); - let mut trailing_empty_line = None; - if line_count >= 2 - && let (Some(prev_line), Some(last_line)) = ( - inline_layout.layout.get(line_count - 2), - inline_layout.layout.get(line_count - 1), - ) - && prev_line.break_reason() == BreakReason::Explicit - && last_line.text_range().is_empty() - && last_line.items().next().is_none() - { - height -= last_line.metrics().line_height; - trailing_empty_line = Some(line_count - 1); - } - - #[cfg(feature = "floats")] - { - if is_bfc_root { - height = height.max(float_height_contribution * scale) - }; - } - - // Note: `width` and `height` are content-box measurements of the inline content. - // `known_dimensions` must not be substituted in here: those are border-box sizes, and - // using them for the scrollable overflow rect (which adds padding below) would - // double-count padding, incorrectly making the container's content overflow it. - let measured_size = taffy::Size { - width: width / scale, - height: height / scale, - }; - - // Unbreakable content (e.g. `white-space: pre` text) may overflow the max advance the - // lines were broken at. `width` is the advance the lines were broken at, so the actual - // extent of the line boxes must be taken from the laid-out lines for `content_size`. - let content_width = f32_max(width, inline_layout.layout.width()) / scale; - - let clamped_size = inputs - .known_dimensions - .or(node_size) - .unwrap_or(measured_size + content_box_inset.sum_axes()) - .maybe_clamp(node_min_size, node_max_size); - let final_size = Size { - width: clamped_size.width, - height: f32_max( - clamped_size.height, - aspect_ratio - .map(|ratio| clamped_size.width / ratio) - .unwrap_or(0.0), - ), - } - .maybe_max(container_pb.sum_axes().map(Some)); - - let container_direction = self.nodes[node_id].layout_style().direction(); - - // `align-content` aligns the line boxes (as a single unit) within the content box in - // the block axis. Floats are positioned relative to the formatting context and are not - // moved. The offset is stored on the text layout for painting and hit-testing. - let align_content = BlockContainerStyle::align_content(&self.nodes[node_id].layout_style()); - let block_offset = if align_content.keyword() == taffy::AlignContentKeyword::Normal { - 0.0 - } else { - let free_space = - final_size.height - content_box_inset.vertical_axis_sum() - measured_size.height; - taffy::compute_block_align_content_offset(align_content, free_space) - }; - inline_layout.block_offset = block_offset; - let line_box_top = container_pb.top + block_offset; - - // Store sizes and positions of inline boxes - let mut ibox_order: u32 = 0; - for line in inline_layout.layout.lines() { - for item in line.items() { - if let parley::layout::PositionedLayoutItem::InlineBox(ibox) = item { - let order = ibox_order; - ibox_order += 1; - let node = &self.nodes[NodeId::from_u64(ibox.id)]; - let style = self.child_layout_style(node); - let padding = style - .padding() - .resolve_or_zero(child_inputs.parent_size, resolve_calc_value); - let border = style - .border() - .resolve_or_zero(child_inputs.parent_size, resolve_calc_value); - let margin = style - .margin() - .resolve_or_zero(child_inputs.parent_size, resolve_calc_value); - - #[cfg(feature = "floats")] - let is_floated = style.float() != Float::None; - #[cfg(not(feature = "floats"))] - let is_floated = false; - - let position = style.position(); - let is_absolute = position.is_out_of_flow(); - let item_direction = style.direction(); - // Inline formatting contexts have no `justify-items`/`align-items` for an - // `auto` self-alignment to defer to, so it behaves as `normal`. - let justify_self = - OofItemStyle::justify_self(&style).unwrap_or(taffy::AlignItems::NORMAL); - let align_self = - OofItemStyle::align_self(&style).unwrap_or(taffy::AlignItems::NORMAL); - - // The static position of an absolutely positioned box depends on the - // display its hypothetical box would have had (the display specified - // before position:absolute blockified it): inline-level boxes sit at - // their position within the line, while block-level boxes start at the - // content-box left edge of their containing block. - let is_inline_level = - style.style.get_box().original_display.outside() == DisplayOutside::Inline; - - // Resolve relative inset offsets against the containing block - // (the content box of the inline container). - let container_content_size = final_size - content_box_inset.sum_axes(); - let inset_style = style.inset(); - let inset = taffy::Rect { - left: inset_style - .left - .maybe_resolve(container_content_size.width, resolve_calc_value), - right: inset_style - .right - .maybe_resolve(container_content_size.width, resolve_calc_value), - top: inset_style - .top - .maybe_resolve(container_content_size.height, resolve_calc_value), - bottom: inset_style - .bottom - .maybe_resolve(container_content_size.height, resolve_calc_value), - }; - let box_inputs = inline_box_inputs(style.size().width, margin, child_inputs); - drop(style); - - if is_absolute { - // The static-position rectangle - // (https://www.w3.org/TR/css-position-3/#staticpos-rect): - // - An inline-level box's rectangle is zero-width at its position - // within the line (`ibox.y` is the baseline as out-of-flow boxes are - // zero-sized) and spans the line box in the block axis. - // - A block-level box's rectangle spans the containing block's content - // box in the inline axis and is zero-height below the line box. - let line_metrics = line.metrics(); - let line_top = (line_metrics.block_min_coord / scale) + line_box_top; - let line_bottom = (line_metrics.block_max_coord / scale) + line_box_top; - let (inline_area, block_area) = if is_inline_level { - let x = (ibox.x / scale) + container_pb.left; - ( - taffy::Line { start: x, end: x }, - taffy::Line { - start: line_top, - end: line_bottom, - }, - ) - } else { - ( - taffy::Line { - start: container_pb.left, - end: final_size.width - container_pb.right, - }, - taffy::Line { - start: line_bottom, - end: line_bottom, - }, - ) - }; - - oof_candidates.push(OofCandidate { - node: taffy::NodeId::from(ibox.id), - order, - position, - static_position: taffy::Point { - x: AxisStaticPosition::from_alignment( - justify_self.resolve_self_relative( - item_direction, - container_direction, - true, - ), - inline_area, - container_direction.is_rtl(), - ), - y: AxisStaticPosition::from_alignment( - align_self.resolve_self_relative( - item_direction, - container_direction, - false, - ), - block_area, - false, - ), - }, - }); - } else if is_floated { - let layout = self.nodes[NodeId::from_u64(ibox.id)].unrounded_layout_mut(); - layout.padding = padding; //.map(|p| p / scale); - layout.border = border; //.map(|p| p / scale); - layout.margin = margin; - } else { - // Re-measure the box to get its border-box size (this hits the layout - // cache). The size cannot be recovered from `ibox` dimensions as the - // space reserved in the line is clamped to be non-negative. - let mut output = - self.compute_child_layout(taffy::NodeId::from(ibox.id), box_inputs); - let size = output.size; - let node = &mut self.nodes[NodeId::from_u64(ibox.id)]; - - let is_relative = position == taffy::Position::Relative; - let inset_offset = if is_relative { - taffy::Point { - x: if container_direction == Direction::Rtl { - inset.right.map(|x| -x).or(inset.left).unwrap_or(0.0) - } else { - inset.left.or(inset.right.map(|x| -x)).unwrap_or(0.0) - }, - y: inset.top.or(inset.bottom.map(|x| -x)).unwrap_or(0.0), - } - } else { - taffy::Point::ZERO - }; - - let layout = node.unrounded_layout_mut(); - layout.size = size; - layout.scrollable_overflow_rect = output.scrollable_overflow_rect; - layout.location.x = - (ibox.x / scale) + margin.left + container_pb.left + inset_offset.x; - // Parley positions the margin box; offset to the border box even - // when a negative top margin makes it extend above the margin box. - layout.location.y = - (ibox.y / scale) + margin.top + line_box_top + inset_offset.y; - layout.padding = padding; //.map(|p| p / scale); - layout.border = border; //.map(|p| p / scale); - layout.margin = margin; - - // Translate anchors from item-relative to container-relative - // coordinates and collect candidates bubbled from the box's subtree - if !output.oof_candidates.is_empty() { - let location = layout.location; - output.oof_candidates.translate(location); - oof_candidates.append(&mut output.oof_candidates); - } - } - } - } - } - - // println!("INLINE LAYOUT FOR {:?}. max_advance: {:?}", node_id, max_advance); - // dbg!(&inline_layout.text); - // println!("Computed: w: {} h: {}", inline_layout.layout.width(), inline_layout.layout.height()); - // println!("known_dimensions: w: {:?} h: {:?}", inputs.known_dimensions.width, inputs.known_dimensions.height); - // println!("\n"); - - let line_baseline = - |line: parley::Line<'_, _>| (line.metrics().baseline / scale) + line_box_top; - let first_baseline = has_inline_content - .then(|| inline_layout.layout.lines().next().map(line_baseline)) - .flatten(); - let last_line_index = trailing_empty_line.unwrap_or(line_count).checked_sub(1); - let last_baseline = has_inline_content - .then(|| last_line_index.and_then(|i| inline_layout.layout.get(i))) - .flatten() - .map(line_baseline); - - // Put layout back - self.nodes[node_id] - .data - .downcast_element_mut() - .unwrap() - .inline_layout_data = Some(inline_layout); - - let oof_position_inset = taffy::Rect { - left: border.left, - right: border.right + scrollbar_gutter.x, - top: border.top, - bottom: border.bottom + scrollbar_gutter.y, + let frame = Frame { + inputs, + node_size, + node_min_size, + node_max_size, + aspect_ratio, + padding, + border, + scrollbar_gutter, + container_pb, + content_box_inset, + child_inputs, + available_space, + collapses_through: has_styles_preventing_being_collapsed_through, + scale, }; - - LayoutOutput { - size: final_size, - scrollable_overflow_rect: { - let content_extent = taffy::Size { - width: content_width, - height: measured_size.height, - } + padding.sum_axes(); - taffy::Rect { - left: 0.0, - right: content_extent.width, - top: 0.0, - bottom: content_extent.height + block_offset.max(0.0), - } - }, - baselines: taffy::Baselines { - first: first_baseline, - last: last_baseline, - }, - top_margin: CollapsibleMarginSet::ZERO, - bottom_margin: CollapsibleMarginSet::ZERO, - margins_can_collapse_through: !has_styles_preventing_being_collapsed_through - && final_size.height == 0.0 - && measured_size.height == 0.0, - oof_candidates, - oof_positioning_area: Some(OofPositioningArea { - size: final_size - oof_position_inset.sum_axes(), - offset: Point { - x: oof_position_inset.left, - y: oof_position_inset.top, - }, - }), - } + TextLayout::compute_layout(self, node_id, inline_layout, frame, block_ctx) } } #[inline(always)] -fn f32_max(a: f32, b: f32) -> f32 { +pub(crate) fn f32_max(a: f32, b: f32) -> f32 { a.max(b) } diff --git a/packages/blitz-dom/src/layout/list.rs b/packages/blitz-dom/src/layout/list.rs index 0d4dbabf1f..43d6a03591 100644 --- a/packages/blitz-dom/src/layout/list.rs +++ b/packages/blitz-dom/src/layout/list.rs @@ -1,6 +1,5 @@ use blitz_traits::node_id::NodeId; use markup5ever::local_name; -use parley::FontFamily; use style::computed_values::list_style_type::T as ListStyleType; use style::{ computed_values::list_style_position::T as ListStylePosition, counter_style::CounterStyle, @@ -9,11 +8,9 @@ use style::{ use crate::{ BaseDocument, node::{ListItemLayout, ListItemLayoutPosition, Marker}, - stylo_to_parley, + text::{MarkerEngine as _, MarkerLayout}, }; -pub(super) const BULLET_FONT_FAMILY: &str = "Bullet, monospace, sans-serif"; - pub(super) fn collect_list_item_children( doc: &mut BaseDocument, index: &mut usize, @@ -78,33 +75,15 @@ fn node_list_item_child( let position = match list_style_position { ListStylePosition::Inside => ListItemLayoutPosition::Inside, ListStylePosition::Outside => { - let mut parley_style = stylo_to_parley::style(child_id, &styles); - - if let Some(font_family) = font_for_bullet_style(list_style_type) { - parley_style.font_family = font_family; - } - - // Create a parley tree builder - let mut font_ctx = doc.font_ctx.lock().unwrap(); - let mut builder = doc.layout_ctx.tree_builder( - &mut font_ctx, + let bullet = list_style_type.0.is_bullet(); + let layout = MarkerLayout::build( + &mut doc.text, + child_id, + &styles, + &marker, + bullet, doc.viewport.scale(), - true, - &parley_style, ); - - match &marker { - Marker::Char(char) => { - let mut buf = [0u8; 4]; - builder.push_text(char.encode_utf8(&mut buf)); - } - Marker::String(str) => builder.push_text(str), - }; - - let mut layout = builder.build().0; - let width = layout.calculate_content_widths().max; - layout.break_all_lines(Some(width)); - ListItemLayoutPosition::Outside(Box::new(layout)) } }; @@ -157,15 +136,6 @@ fn marker_for_style(list_style_type: ListStyleType, index: usize) -> Option Option> { - if list_style_type.0.is_bullet() { - return Some(BULLET_FONT_FAMILY.into()); - } - - None -} - const ALPHABET: [char; 26] = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', diff --git a/packages/blitz-dom/src/layout/mod.rs b/packages/blitz-dom/src/layout/mod.rs index 0a5fbc1dc8..eb4ae602e9 100644 --- a/packages/blitz-dom/src/layout/mod.rs +++ b/packages/blitz-dom/src/layout/mod.rs @@ -5,6 +5,7 @@ //! This is slower, yes, but happens fast enough that it's not a huge issue. use crate::node::{ComputedStyleRef, ImageData, NodeData, SpecialElementData}; +use crate::text::EditEngine as _; use crate::{document::BaseDocument, dom_node_id, node::Node, taffy_node_id}; use markup5ever::{LocalName, local_name}; use std::cell::Ref; @@ -82,7 +83,7 @@ pub(crate) fn resolve_calc_value(calc_ptr: *const (), parent_size: f32) -> f32 { /// Per-pass layout state: wraps the document for the duration of a layout pass and carries /// the Taffy tree trait implementations. Derefs to [`BaseDocument`]. pub(crate) struct LayoutPassState<'doc> { - doc: &'doc mut BaseDocument, + pub(crate) doc: &'doc mut BaseDocument, /// The writing mode of the box whose layout algorithm is currently running (see /// `layout::writing_mode`). Child styles read during that algorithm are expressed in its axes. #[cfg(feature = "writing-mode")] @@ -316,11 +317,9 @@ impl LayoutPassState<'_> { .downcast_element_mut() .and_then(|el| el.text_input_data_mut()) { - input.editor.set_width(Some(content_width * scale)); - input.editor.refresh_layout( - &mut doc.font_ctx.lock().unwrap(), - &mut doc.layout_ctx, - ); + input + .editor + .set_width(&mut doc.text, Some(content_width * scale)); } } return output; diff --git a/packages/blitz-dom/src/layout/text_transform.rs b/packages/blitz-dom/src/layout/text_transform.rs index cafc86b0bd..5968f179c6 100644 --- a/packages/blitz-dom/src/layout/text_transform.rs +++ b/packages/blitz-dom/src/layout/text_transform.rs @@ -11,7 +11,6 @@ use icu_locale_core::LanguageIdentifier; use icu_properties::props::{GeneralCategory, GeneralCategoryGroup}; use icu_properties::{CodePointMapData, CodePointMapDataBorrowed}; use icu_segmenter::{WordSegmenter, WordSegmenterBorrowed, options::WordBreakInvariantOptions}; -use parley::{Brush, TreeBuilder}; use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; use style::properties::ComputedValues; use style::values::computed::TextTransform; @@ -98,12 +97,20 @@ fn casing_language(lang: LanguageIdentifier) -> LanguageIdentifier { } } +/// The text pushed so far to a text backend's builder, which `capitalize` reads as context. +pub(crate) trait PushedText { + /// The text pushed so far. + fn text(&self) -> &str; + /// Whether collapsible white space is pending at the end of the text, which ends a word. + fn has_pending_whitespace(&self) -> bool; +} + /// Applies text transforms to the sequence of text nodes in an inline formatting context. /// /// `capitalize` operates on words, which may span multiple text nodes, so the text already -/// pushed to the [`TreeBuilder`] is used as context for word segmentation. Only look-behind -/// is needed: whether a word boundary precedes a letter never depends on the text that -/// follows it. +/// pushed to the builder ([`PushedText`]) is used as context for word segmentation. Only +/// look-behind is needed: whether a word boundary precedes a letter never depends on the text +/// that follows it. #[derive(Default)] pub(crate) struct TextTransformer { /// The offset in the builder's text of the last forced word boundary. @@ -116,17 +123,17 @@ pub(crate) struct TextTransformer { impl TextTransformer { /// Forces a word boundary (e.g. at an atomic inline or a forced line break). - pub(crate) fn word_break(&mut self, builder: &TreeBuilder<'_, B>) { + pub(crate) fn word_break(&mut self, builder: &impl PushedText) { self.context_start = builder.text().len(); } /// Transforms the content of a text node that is about to be pushed to `builder`. #[inline] - pub(crate) fn transform<'a, B: Brush>( + pub(crate) fn transform<'a>( &'a mut self, text: &'a str, transform: &CaseTransform, - builder: &TreeBuilder<'_, B>, + builder: &impl PushedText, ) -> &'a str { if transform.text_transform.is_empty() { return text; @@ -134,11 +141,11 @@ impl TextTransformer { self.transform_nonempty(text, transform, builder) } - fn transform_nonempty<'a, B: Brush>( + fn transform_nonempty<'a>( &'a mut self, text: &'a str, transform: &CaseTransform, - builder: &TreeBuilder<'_, B>, + builder: &impl PushedText, ) -> &'a str { let case = transform.text_transform.case(); if text.is_ascii() @@ -536,10 +543,10 @@ fn ceil_char_boundary(s: &str, mut index: usize) -> usize { index } -#[cfg(test)] +#[cfg(all(test, text_parley))] mod tests { use super::*; - use parley::{FontContext, LayoutContext, TextStyle}; + use parley::{FontContext, LayoutContext, TextStyle, TreeBuilder}; fn transform(kind: TextTransform, lang: &str, texts: &[&str]) -> Vec { let transform = CaseTransform { diff --git a/packages/blitz-dom/src/lib.rs b/packages/blitz-dom/src/lib.rs index 792aaf10b7..d4200cf0f9 100644 --- a/packages/blitz-dom/src/lib.rs +++ b/packages/blitz-dom/src/lib.rs @@ -44,7 +44,6 @@ mod config; mod cssom; mod debug; mod events; -mod font_metrics; mod form; mod html; /// Loading of `