use blitz_traits::node_id::NodeId;
use cssparser::ParserInput;
use kurbo::{Affine, Rect as KurboRect};
use linebender_resource_handle::Blob;
use markup5ever::{LocalName, QualName, local_name};
use selectors::matching::{ElementSelectorFlags, QuirksMode};
use std::cell::Cell;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use style::Atom;
use style::parser::ParserContext;
use style::properties::ComputedValues;
use style::properties::{Importance, PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
use style::stylesheets::{DocumentStyleSheet, Origin, UrlExtraData};
use style::values::computed::Display as StyloDisplay;
use style::{
properties::{PropertyDeclarationBlock, parse_style_attribute},
servo_arc::Arc as ServoArc,
shared_lock::{Locked, SharedRwLock},
stylesheets::CssRuleType,
};
use style_dom::ElementState;
use style_traits::ParsingMode;
use taffy::{
Cache,
prelude::{Layout, Style},
};
use url::Url;
use super::stylo_data::StyloData;
use super::{Attribute, Attributes};
use crate::Document;
use crate::layout::table::TableContext;
use crate::node::{TextBrush, TextInputData, TextLayout};
#[cfg(feature = "shadow-dom")]
use super::custom_element::CustomElementData;
#[cfg(feature = "custom-widget")]
use super::custom_widget::CustomWidgetData;
macro_rules! local_names {
($($name:tt),+) => {
[$(local_name!($name),)+]
};
}
pub struct ElementData {
pub name: QualName,
pub id: Option<Atom>,
pub attrs: Attributes,
pub is_focussable: bool,
pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
pub special_data: SpecialElementData,
pub background_images: Vec<Option<ImageResourceData>>,
pub mask_images: Vec<Option<ImageResourceData>>,
pub inline_layout_data: Option<Box<TextLayout>>,
pub list_item_data: Option<Box<ListItemLayout>>,
pub template_contents: Option<NodeId>,
pub shadow_root: Option<NodeId>,
pub assigned_slot: Option<NodeId>,
pub stylo_element_data: StyloData,
pub selector_flags: Cell<ElementSelectorFlags>,
pub guard: Option<SharedRwLock>,
pub element_state: ElementState,
pub has_snapshot: bool,
pub snapshot_handled: AtomicBool,
pub dirty_descendants: AtomicBool,
pub before: Option<NodeId>,
pub after: Option<NodeId>,
pub detailed_grid_info: Option<Box<taffy::DetailedGridInfo>>,
pub style: Style<Atom>,
pub subtree_hoists: bool,
pub style_source: Option<ServoArc<ComputedValues>>,
pub display_constructed_as: StyloDisplay,
pub cache: Cache,
pub unrounded_layout: Layout,
pub final_layout: Layout,
pub scroll_offset: crate::Point<f64>,
pub scrollable_overflow: KurboRect,
pub transform: Option<Affine>,
}
pub struct DocumentData {
pub stylo_element_data: StyloData,
pub selector_flags: Cell<ElementSelectorFlags>,
pub guard: Option<SharedRwLock>,
pub dirty_descendants: AtomicBool,
pub element_state: ElementState,
pub has_snapshot: bool,
pub snapshot_handled: AtomicBool,
pub style: Style<Atom>,
pub subtree_hoists: bool,
pub style_source: Option<ServoArc<ComputedValues>>,
pub display_constructed_as: StyloDisplay,
pub cache: Cache,
pub unrounded_layout: Layout,
pub final_layout: Layout,
pub scroll_offset: crate::Point<f64>,
pub scrollable_overflow: KurboRect,
pub transform: Option<Affine>,
}
impl std::fmt::Debug for DocumentData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DocumentData")
.field("stylo_element_data", &self.stylo_element_data)
.field("guard", &self.guard)
.field("dirty_descendants", &self.dirty_descendants)
.field("element_state", &self.element_state)
.field("has_snapshot", &self.has_snapshot)
.field("snapshot_handled", &self.snapshot_handled)
.field("style", &self.style)
.field("display_constructed_as", &self.display_constructed_as)
.field("cache", &self.cache)
.field("unrounded_layout", &self.unrounded_layout)
.field("final_layout", &self.final_layout)
.field("scroll_offset", &self.scroll_offset)
.field("scrollable_overflow", &self.scrollable_overflow)
.field("transform", &self.transform)
.finish_non_exhaustive()
}
}
impl DocumentData {
pub fn new() -> Self {
Self {
stylo_element_data: Default::default(),
selector_flags: Cell::new(ElementSelectorFlags::empty()),
guard: None,
dirty_descendants: AtomicBool::new(true),
element_state: ElementState::empty(),
has_snapshot: false,
snapshot_handled: AtomicBool::new(false),
style: Default::default(),
style_source: None,
subtree_hoists: false,
display_constructed_as: StyloDisplay::Block,
cache: Cache::new(),
unrounded_layout: Layout::new(),
final_layout: Layout::new(),
scroll_offset: crate::Point::ZERO,
scrollable_overflow: KurboRect::ZERO,
transform: None,
}
}
}
impl Default for DocumentData {
fn default() -> Self {
Self::new()
}
}
impl Clone for DocumentData {
fn clone(&self) -> Self {
Self {
guard: self.guard.clone(),
..Self::new()
}
}
}
impl std::fmt::Debug for ElementData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ElementData")
.field("name", &self.name)
.field("id", &self.id)
.field("attrs", &self.attrs)
.field("is_focussable", &self.is_focussable)
.field("style_attribute", &self.style_attribute)
.field("special_data", &self.special_data)
.field("background_images", &self.background_images)
.field("mask_images", &self.mask_images)
.field("inline_layout_data", &self.inline_layout_data)
.field("list_item_data", &self.list_item_data)
.field("template_contents", &self.template_contents)
.field("element_state", &self.element_state)
.field("display_constructed_as", &self.display_constructed_as)
.finish_non_exhaustive()
}
}
impl Clone for ElementData {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
id: self.id.clone(),
attrs: self.attrs.clone(),
is_focussable: self.is_focussable,
style_attribute: self.style_attribute.clone(),
special_data: self.special_data.clone(),
background_images: self.background_images.clone(),
mask_images: self.mask_images.clone(),
inline_layout_data: self.inline_layout_data.clone(),
list_item_data: self.list_item_data.clone(),
template_contents: self.template_contents,
shadow_root: None,
assigned_slot: None,
stylo_element_data: Default::default(),
selector_flags: Cell::new(ElementSelectorFlags::empty()),
guard: self.guard.clone(),
element_state: self.element_state,
has_snapshot: false,
snapshot_handled: AtomicBool::new(false),
dirty_descendants: AtomicBool::new(true),
before: None,
after: None,
detailed_grid_info: None,
style: Default::default(),
style_source: None,
subtree_hoists: false,
display_constructed_as: StyloDisplay::Block,
cache: Cache::new(),
unrounded_layout: Layout::new(),
final_layout: Layout::new(),
scroll_offset: crate::Point::ZERO,
scrollable_overflow: KurboRect::ZERO,
transform: None,
}
}
}
#[derive(Copy, Clone, Default)]
#[non_exhaustive]
pub enum SpecialElementType {
Stylesheet,
Image,
Canvas,
TableRoot,
TextInput,
CheckboxInput,
#[cfg(feature = "file-input")]
FileInput,
#[default]
None,
}
#[derive(Default)]
pub enum SpecialElementData {
SubDocument(Box<dyn Document>),
#[cfg(feature = "custom-widget")]
CustomWidget(CustomWidgetData),
#[cfg(feature = "shadow-dom")]
CustomElement(CustomElementData),
Stylesheet(DocumentStyleSheet),
Image(Box<ImageData>),
Canvas(CanvasData),
TableRoot(Arc<TableContext>),
TextInput(TextInputData),
CheckboxInput(bool),
#[cfg(feature = "file-input")]
FileInput(FileData),
#[default]
None,
}
impl Clone for SpecialElementData {
fn clone(&self) -> Self {
match self {
Self::SubDocument(_) => Self::None, #[cfg(feature = "custom-widget")]
Self::CustomWidget(_) => Self::None, #[cfg(feature = "shadow-dom")]
Self::CustomElement(_) => Self::None, Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
Self::Image(data) => Self::Image(data.clone()),
Self::Canvas(data) => Self::Canvas(data.clone()),
Self::TableRoot(data) => Self::TableRoot(data.clone()),
Self::TextInput(data) => Self::TextInput(data.clone()),
Self::CheckboxInput(data) => Self::CheckboxInput(*data),
#[cfg(feature = "file-input")]
Self::FileInput(data) => Self::FileInput(data.clone()),
Self::None => Self::None,
}
}
}
impl SpecialElementData {
pub fn take(&mut self) -> Self {
std::mem::take(self)
}
}
impl ElementData {
pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
let id_attr_atom = attrs
.iter()
.find(|attr| &attr.name.local == "id")
.map(|attr| attr.value.as_ref())
.map(|value: &str| Atom::from(value));
let mut data = ElementData {
name,
id: id_attr_atom,
attrs: Attributes::new(attrs),
is_focussable: false,
style_attribute: Default::default(),
inline_layout_data: None,
list_item_data: None,
special_data: SpecialElementData::None,
template_contents: None,
shadow_root: None,
assigned_slot: None,
background_images: Vec::new(),
mask_images: Vec::new(),
stylo_element_data: Default::default(),
selector_flags: Cell::new(ElementSelectorFlags::empty()),
guard: None,
element_state: ElementState::empty(),
has_snapshot: false,
snapshot_handled: AtomicBool::new(false),
dirty_descendants: AtomicBool::new(true),
before: None,
after: None,
detailed_grid_info: None,
style: Default::default(),
style_source: None,
subtree_hoists: false,
display_constructed_as: StyloDisplay::Block,
cache: Cache::new(),
unrounded_layout: Layout::new(),
final_layout: Layout::new(),
scroll_offset: crate::Point::ZERO,
scrollable_overflow: KurboRect::ZERO,
transform: None,
};
data.flush_is_focussable();
if data.can_be_disabled() {
data.element_state
.insert(match data.has_attr(local_name!("disabled")) {
true => ElementState::DISABLED,
false => ElementState::ENABLED,
});
}
data
}
pub fn attrs(&self) -> &[Attribute] {
&self.attrs
}
pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
Some(&attr.value)
}
pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
attr.value.parse::<T>().ok()
}
pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
self.attrs.iter().any(|attr| name == attr.name.local)
}
pub fn can_be_disabled(&self) -> bool {
local_names!("button", "input", "select", "textarea").contains(&self.name.local)
}
pub fn image_data(&self) -> Option<&ImageData> {
match &self.special_data {
SpecialElementData::Image(data) => Some(&**data),
_ => None,
}
}
pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
match self.special_data {
SpecialElementData::Image(ref mut data) => Some(&mut **data),
_ => None,
}
}
pub fn raster_image_data(&self) -> Option<&RasterImageData> {
match self.image_data()? {
ImageData::Raster(data) => Some(data),
_ => None,
}
}
pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
match self.image_data_mut()? {
ImageData::Raster(data) => Some(data),
_ => None,
}
}
pub fn canvas_data(&self) -> Option<&CanvasData> {
match &self.special_data {
SpecialElementData::Canvas(data) => Some(data),
_ => None,
}
}
pub fn sub_doc_data(&self) -> Option<&dyn Document> {
match &self.special_data {
SpecialElementData::SubDocument(data) => Some(data.as_ref()),
_ => None,
}
}
pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
match &mut self.special_data {
SpecialElementData::SubDocument(data) => Some(data.as_mut()),
_ => None,
}
}
#[cfg(feature = "svg")]
pub fn svg_data(&self) -> Option<&usvg::Tree> {
match self.image_data()? {
ImageData::Svg(data) => Some(&data.tree),
_ => None,
}
}
pub fn text_input_data(&self) -> Option<&TextInputData> {
match &self.special_data {
SpecialElementData::TextInput(data) => Some(data),
_ => None,
}
}
pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
match &mut self.special_data {
SpecialElementData::TextInput(data) => Some(data),
_ => None,
}
}
#[cfg(feature = "custom-widget")]
pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
match &self.special_data {
SpecialElementData::CustomWidget(data) => Some(data),
_ => None,
}
}
#[cfg(feature = "custom-widget")]
pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
match &mut self.special_data {
SpecialElementData::CustomWidget(data) => Some(data),
_ => None,
}
}
#[cfg(feature = "shadow-dom")]
pub fn custom_element_data(&self) -> Option<&CustomElementData> {
match &self.special_data {
SpecialElementData::CustomElement(data) => Some(data),
_ => None,
}
}
#[cfg(feature = "shadow-dom")]
pub fn custom_element_data_mut(&mut self) -> Option<&mut CustomElementData> {
match &mut self.special_data {
SpecialElementData::CustomElement(data) => Some(data),
_ => None,
}
}
pub fn checkbox_input_checked(&self) -> Option<bool> {
match self.special_data {
SpecialElementData::CheckboxInput(checked) => Some(checked),
_ => None,
}
}
pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
match self.special_data {
SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
_ => None,
}
}
#[cfg(feature = "file-input")]
pub fn file_data(&self) -> Option<&FileData> {
match &self.special_data {
SpecialElementData::FileInput(data) => Some(data),
_ => None,
}
}
#[cfg(feature = "file-input")]
pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
match &mut self.special_data {
SpecialElementData::FileInput(data) => Some(data),
_ => None,
}
}
pub fn flush_is_focussable(&mut self) {
let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
let contains_sub_document: bool = self.sub_doc_data().is_some();
self.is_focussable = contains_sub_document
|| (!disabled
&& match tabindex {
Some(index) => index >= 0,
None => {
if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
self.attr(local_name!("href")).is_some()
} else {
const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
local_name!("button"),
local_name!("input"),
local_name!("select"),
local_name!("textarea"),
local_name!("frame"),
local_name!("iframe"),
local_name!("summary"),
];
DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
}
}
})
}
pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
ServoArc::new(guard.wrap(parse_style_attribute(
style_str,
url_extra_data,
None,
QuirksMode::NoQuirks,
CssRuleType::Style,
)))
});
}
pub fn set_style_property(
&mut self,
name: &str,
value: &str,
guard: &SharedRwLock,
url_extra_data: UrlExtraData,
) -> bool {
let context = ParserContext::new(
Origin::Author,
&url_extra_data,
Some(CssRuleType::Style),
ParsingMode::DEFAULT,
QuirksMode::NoQuirks,
Default::default(),
None,
None,
Default::default(),
);
let Ok(property_id) = PropertyId::parse(name, &context) else {
#[cfg(feature = "tracing")]
tracing::warn!(property = name, "Unsupported property");
return false;
};
let mut source_property_declaration = SourcePropertyDeclaration::default();
let mut input = ParserInput::new(value);
let mut parser = style::values::Parser::new(&mut input);
let Ok(_) = PropertyDeclaration::parse_into(
&mut source_property_declaration,
property_id,
&context,
&mut parser,
) else {
#[cfg(feature = "tracing")]
tracing::warn!(property = name, value, "Invalid property value");
return false;
};
if self.style_attribute.is_none() {
self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
}
self.style_attribute
.as_mut()
.unwrap()
.write_with(&mut guard.write())
.extend(source_property_declaration.drain(), Importance::Normal);
true
}
pub fn remove_style_property(
&mut self,
name: &str,
guard: &SharedRwLock,
url_extra_data: UrlExtraData,
) -> bool {
let context = ParserContext::new(
Origin::Author,
&url_extra_data,
Some(CssRuleType::Style),
ParsingMode::DEFAULT,
QuirksMode::NoQuirks,
Default::default(),
None,
None,
Default::default(),
);
let Ok(property_id) = PropertyId::parse(name, &context) else {
#[cfg(feature = "tracing")]
tracing::warn!(property = name, "Unsupported property");
return false;
};
if let Some(style) = &mut self.style_attribute {
let mut guard = guard.write();
let style = style.write_with(&mut guard);
if let Some(index) = style.first_declaration_to_remove(&property_id) {
style.remove_property(&property_id, index);
return true;
}
}
false
}
pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
self.special_data = SpecialElementData::SubDocument(sub_document);
}
pub fn remove_sub_document(&mut self) {
self.special_data = SpecialElementData::None;
}
#[cfg(feature = "custom-widget")]
pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
use crate::node::custom_widget::CustomWidgetData;
self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
}
#[cfg(feature = "custom-widget")]
pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
let resource_ids = self
.custom_widget_data_mut()
.map(|widget_data| widget_data.take_resource_ids())
.unwrap_or_default();
self.special_data = SpecialElementData::None;
resource_ids
}
pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
std::mem::take(&mut self.inline_layout_data)
}
pub fn is_submit_button(&self) -> bool {
if self.name.local != local_name!("button") {
return false;
}
let type_attr = self.attr(local_name!("type"));
let is_submit = type_attr == Some("submit");
let is_auto_submit = type_attr.is_none()
&& self.attr(LocalName::from("command")).is_none()
&& self.attr(LocalName::from("commandfor")).is_none();
is_submit || is_auto_submit
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RasterImageData {
pub width: u32,
pub height: u32,
pub data: Blob<u8>,
}
impl RasterImageData {
pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
Self {
width,
height,
data: Blob::new(data),
}
}
}
#[cfg(feature = "svg")]
#[derive(Debug, Clone)]
pub struct SvgImageData {
pub tree: Arc<usvg::Tree>,
}
#[cfg(feature = "svg")]
impl SvgImageData {
pub fn intrinsic_width(&self) -> Option<f32> {
use usvg::svgtypes::LengthUnit;
let declared = self
.tree
.intrinsic_dimensions()
.width
.is_some_and(|len| len.unit != LengthUnit::Percent);
declared.then(|| self.tree.size().width())
}
pub fn intrinsic_height(&self) -> Option<f32> {
use usvg::svgtypes::LengthUnit;
let declared = self
.tree
.intrinsic_dimensions()
.height
.is_some_and(|len| len.unit != LengthUnit::Percent);
declared.then(|| self.tree.size().height())
}
pub fn viewbox_aspect_ratio(&self) -> Option<f32> {
self.tree
.intrinsic_dimensions()
.view_box
.map(|vb| vb.width() / vb.height())
}
pub fn resolved_width(&self, container_width: Option<f32>) -> Option<f32> {
use usvg::svgtypes::LengthUnit;
match self.tree.intrinsic_dimensions().width {
Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().width()),
Some(len) => container_width.map(|cw| cw * (len.number as f32) / 100.0),
None => None,
}
}
pub fn resolved_height(&self, container_height: Option<f32>) -> Option<f32> {
use usvg::svgtypes::LengthUnit;
match self.tree.intrinsic_dimensions().height {
Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().height()),
Some(len) => container_height.map(|ch| ch * (len.number as f32) / 100.0),
None => None,
}
}
pub fn aspect_ratio(&self) -> f32 {
match (self.intrinsic_width(), self.intrinsic_height()) {
(Some(w), Some(h)) => w / h,
_ => self.viewbox_aspect_ratio().unwrap_or_else(|| {
let size = self.tree.size();
size.width() / size.height()
}),
}
}
pub fn intrinsic_size(&self) -> (f32, f32) {
let aspect_ratio = self.aspect_ratio();
match (self.intrinsic_width(), self.intrinsic_height()) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) => (w, w / aspect_ratio),
(None, Some(h)) => (h * aspect_ratio, h),
(None, None) => {
if self.viewbox_aspect_ratio().is_some() {
let scale = (300.0 / aspect_ratio).min(150.0);
(scale * aspect_ratio, scale)
} else {
let size = self.tree.size();
(size.width(), size.height())
}
}
}
}
}
#[derive(Debug, Clone)]
pub enum ImageData {
Raster(RasterImageData),
#[cfg(feature = "svg")]
Svg(SvgImageData),
None,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Status {
Ok,
Error,
Loading,
}
#[derive(Debug, Clone)]
pub struct ImageResourceData {
pub url: ServoArc<Url>,
pub status: Status,
pub image: ImageData,
}
impl ImageResourceData {
pub fn new(url: ServoArc<Url>) -> Self {
Self {
url,
status: Status::Loading,
image: ImageData::None,
}
}
}
#[derive(Debug, Clone)]
pub struct CanvasData {
pub custom_paint_source_id: u64,
}
impl std::fmt::Debug for SpecialElementData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
#[cfg(feature = "custom-widget")]
SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
#[cfg(feature = "shadow-dom")]
SpecialElementData::CustomElement(_) => f.write_str("NodeSpecificData::CustomElement"),
SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
SpecialElementData::Image(data) => match **data {
ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
#[cfg(feature = "svg")]
ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
},
SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
#[cfg(feature = "file-input")]
SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
SpecialElementData::None => f.write_str("NodeSpecificData::None"),
}
}
}
#[derive(Clone)]
pub struct ListItemLayout {
pub marker: Marker,
pub position: ListItemLayoutPosition,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Marker {
Char(char),
String(String),
}
#[derive(Clone)]
pub enum ListItemLayoutPosition {
Inside,
Outside(Box<parley::Layout<TextBrush>>),
}
impl std::fmt::Debug for ListItemLayout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ListItemLayout - marker {:?}", self.marker)
}
}
#[cfg(feature = "file-input")]
mod file_data {
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct FileData(pub Vec<PathBuf>);
impl Deref for FileData {
type Target = Vec<PathBuf>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FileData {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Vec<PathBuf>> for FileData {
fn from(files: Vec<PathBuf>) -> Self {
Self(files)
}
}
}
#[cfg(feature = "file-input")]
pub use file_data::FileData;
#[cfg(test)]
mod tests {
use super::TextInputData;
use parley::{FontContext, LayoutContext};
fn make_input(is_multiline: bool, text: &str) -> TextInputData {
let mut font_ctx = FontContext::new();
let mut layout_ctx = LayoutContext::new();
let mut data = TextInputData::new(is_multiline);
data.editor.set_scale(1.0);
data.editor.set_text(text);
data.editor
.driver(&mut font_ctx, &mut layout_ctx)
.refresh_layout();
data
}
#[test]
fn short_text_does_not_scroll() {
let mut data = make_input(false, "hi");
data.clamp_scroll_offset(1000.0, 100.0);
assert_eq!(data.scroll_offset, 0.0);
}
#[test]
fn single_line_scrolls_to_follow_caret() {
let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
let mut data = make_input(false, text);
let content_box_width = 40.0;
let content_box_height = 20.0;
data.editor
.driver(&mut FontContext::new(), &mut LayoutContext::new())
.move_to_text_end();
data.clamp_scroll_offset(content_box_width, content_box_height);
let layout_width = data.editor.try_layout().unwrap().full_width();
if layout_width > content_box_width {
assert!(
data.scroll_offset > 0.0,
"expected horizontal scroll for overflowing single-line input"
);
let caret = data.editor.cursor_geometry(1.5).unwrap();
assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
}
data.editor
.driver(&mut FontContext::new(), &mut LayoutContext::new())
.move_to_text_start();
data.clamp_scroll_offset(content_box_width, content_box_height);
assert_eq!(data.scroll_offset, 0.0);
}
#[test]
fn multiline_scrolls_vertically_not_horizontally() {
let text = (0..40)
.map(|i| format!("line {i}"))
.collect::<Vec<_>>()
.join("\n");
let mut data = make_input(true, &text);
data.editor.set_width(Some(200.0));
data.editor
.driver(&mut FontContext::new(), &mut LayoutContext::new())
.refresh_layout();
let content_box_width = 200.0;
let content_box_height = 30.0;
data.editor
.driver(&mut FontContext::new(), &mut LayoutContext::new())
.move_to_text_end();
data.clamp_scroll_offset(content_box_width, content_box_height);
let layout_height = data.editor.try_layout().unwrap().height();
if layout_height > content_box_height {
assert!(
data.scroll_offset > 0.0,
"expected vertical scroll for overflowing multi-line input"
);
}
}
#[test]
fn scroll_by_clamps_and_bubbles() {
let text = (0..40)
.map(|i| format!("line {i}"))
.collect::<Vec<_>>()
.join("\n");
let mut data = make_input(true, &text);
data.editor.set_width(Some(200.0));
data.editor
.driver(&mut FontContext::new(), &mut LayoutContext::new())
.refresh_layout();
let content_box_width = 200.0;
let content_box_height = 30.0;
let max = data.max_scroll_offset(content_box_width, content_box_height);
assert!(max > 0.0, "test text should overflow the content box");
assert_eq!(data.scroll_offset, 0.0);
let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
assert_eq!(data.scroll_offset, 0.0);
assert_eq!(bubbled, 15.0);
let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
assert_eq!(data.scroll_offset, 10.0);
assert_eq!(bubbled, 0.0);
let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
assert_eq!(data.scroll_offset, max);
assert!((bubbled - (-110.0)).abs() < 1e-3);
}
#[test]
fn single_line_does_not_scroll_when_text_fits() {
let mut data = make_input(false, "hi");
let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
assert_eq!(data.scroll_offset, 0.0);
assert_eq!(bubbled, -50.0);
}
}