diff --git a/core/src/html.rs b/core/src/html.rs index 274fa4761..d021a578a 100644 --- a/core/src/html.rs +++ b/core/src/html.rs @@ -2,6 +2,7 @@ mod dimensions; mod iterators; +mod layout; mod text_format; pub use text_format::{FormatSpans, TextFormat}; diff --git a/core/src/html/layout.rs b/core/src/html/layout.rs new file mode 100644 index 000000000..7267b2d5d --- /dev/null +++ b/core/src/html/layout.rs @@ -0,0 +1,267 @@ +//! Layout box structure + +use crate::context::UpdateContext; +use crate::font::Font; +use crate::html::dimensions::{BoxBounds, Position, Size}; +use crate::html::text_format::{FormatSpans, TextFormat, TextSpan}; +use crate::tag_utils::SwfMovie; +use gc_arena::{Collect, GcCell, MutationContext}; +use std::sync::Arc; +use swf::Twips; + +/// Contains information relating to the current layout operation. +pub struct LayoutContext<'gc> { + /// The position to put text into. + cursor: Position, + + /// The resolved font object to use when measuring text. + font: Option>, + + /// The start of the current chain of layout boxes. + first_box: Option>>, + + /// The end of the current chain of layout boxes. + last_box: Option>>, +} + +impl<'gc> Default for LayoutContext<'gc> { + fn default() -> Self { + Self { + cursor: Default::default(), + font: None, + first_box: None, + last_box: None, + } + } +} + +impl<'gc> LayoutContext<'gc> { + fn cursor(&self) -> &Position { + &self.cursor + } + + fn cursor_mut(&mut self) -> &mut Position { + &mut self.cursor + } + + fn font(&self) -> Option> { + self.font + } + + fn resolve_font( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + movie: Arc, + name: &str, + ) -> Option> { + let library = context.library.library_for_movie_mut(movie); + if let Some(font) = library + .get_font_by_name(name) + .or_else(|| library.device_font()) + { + self.font = Some(font); + return self.font; + } + + None + } + + fn append_box( + &mut self, + gc_context: MutationContext<'gc, '_>, + to_append: GcCell<'gc, LayoutBox<'gc>>, + ) { + if self.first_box.is_none() { + self.first_box = Some(to_append); + } + + if let Some(last) = self.last_box { + last.write(gc_context).next_sibling = Some(to_append); + } + + self.last_box = Some(to_append); + } + + fn end_layout(self) -> Option>> { + self.first_box + } +} + +/// A `LayoutBox` represents a series of nested content boxes, each of which +/// may contain a single line of text with a given text format applied to it. +/// +/// Layout boxes are nested and can optionally be associated with an HTML +/// element. The relationship between elements and boxes are nullably +/// one-to-many: an HTML element may be represented by multiple layout boxes, +/// while a layout may have zero or one HTML elements attached to it. This +/// allows inline content +/// +/// They also have margins, padding, and borders which are calculated and +/// rendered according to CSS spec. +/// +/// For example, an HTML tree that looks like this: +/// +/// ```

I'm a layout element node!

``` +/// +/// produces a top-level `LayoutBox` for the `

` tag, which contains one or +/// more generated boxes for each run of text. The `` tag is cut at it's +/// whitespace if necessary and the cut pieces are added to their respective +/// generated boxes. +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct LayoutBox<'gc> { + /// The rectangle corresponding to the outer boundaries of the + bounds: BoxBounds, + + /// The layout box to be placed after this one. + next_sibling: Option>>, + + /// What content is contained by the content box. + content: LayoutContent<'gc>, +} + +#[derive(Clone, Debug, Collect)] +#[collect(require_static)] +pub struct CollecTwips(Twips); + +/// Represents different content modes of a given layout box. +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub enum LayoutContent<'gc> { + /// A layout box containing some part of a text span. + /// + /// The text is assumed to be pulled from the same `FormatSpans` + Text { + /// The start position of the text to render. + start: usize, + end: usize, + text_format: TextFormat, + font: Font<'gc>, + font_size: CollecTwips, + }, +} + +impl<'gc> LayoutBox<'gc> { + /// Construct a text box for an HTML text node. + pub fn from_text( + mc: MutationContext<'gc, '_>, + start: usize, + end: usize, + text_format: TextFormat, + font: Font<'gc>, + font_size: Twips, + ) -> GcCell<'gc, Self> { + GcCell::allocate( + mc, + Self { + bounds: Default::default(), + next_sibling: None, + content: LayoutContent::Text { + start, + end, + text_format, + font, + font_size: CollecTwips(font_size), + }, + }, + ) + } + + /// Returns the next sibling box. + pub fn next_sibling(&self) -> Option>> { + self.next_sibling + } + + /// + pub fn append_text_fragment( + mc: MutationContext<'gc, '_>, + lc: &mut LayoutContext<'gc>, + text: &str, + start: usize, + end: usize, + span: &TextSpan, + ) { + let font_size = Twips::from_pixels(span.size); + let text_size = Size::from(lc.font().unwrap().measure(text, font_size)); + let text_bounds = BoxBounds::from_position_and_size(*lc.cursor(), text_size); + let new_text = Self::from_text( + mc, + start, + end, + span.get_text_format(), + lc.font().unwrap(), + font_size, + ); + let mut write = new_text.write(mc); + + write.bounds = text_bounds; + + *lc.cursor_mut() += Position::from((text_size.width(), Twips::default())); + lc.append_box(mc, new_text); + } + + /// Construct a new layout hierarchy from text spans. + pub fn lower_from_text_spans( + fs: &FormatSpans, + context: &mut UpdateContext<'_, 'gc, '_>, + movie: Arc, + bounds: Twips, + ) -> Option>> { + let mut layout_context: LayoutContext = Default::default(); + + for (start, _end, text, span) in fs.iter_spans() { + if let Some(font) = layout_context.resolve_font(context, movie.clone(), &span.font) { + let font_size = Twips::from_pixels(span.size); + let breakpoint_list = + font.split_wrapped_lines(&text, font_size, bounds, layout_context.cursor().x()); + + let end = text.len(); + + let mut last_breakpoint = 0; + + for breakpoint in breakpoint_list { + if last_breakpoint != breakpoint { + Self::append_text_fragment( + context.gc_context, + &mut layout_context, + &text[last_breakpoint..breakpoint], + start + last_breakpoint, + start + breakpoint, + span, + ); + } + + last_breakpoint = breakpoint; + } + + Self::append_text_fragment( + context.gc_context, + &mut layout_context, + &text[last_breakpoint..end], + start + last_breakpoint, + start + end, + span, + ); + } + } + + layout_context.end_layout() + } + + pub fn bounds(&self) -> BoxBounds { + self.bounds + } + + /// Returns a reference to the text this box contains. + pub fn text_node(&self) -> Option<(usize, usize, &TextFormat, Font<'gc>, Twips)> { + match &self.content { + LayoutContent::Text { + start, + end, + text_format, + font, + font_size, + } => Some((*start, *end, &text_format, *font, font_size.0)), + } + } +}