Add a system of layout boxes to store text layout decisions made at update time.
This commit is contained in:
parent
4a2fac28d1
commit
8622cb97a9
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
mod dimensions;
|
mod dimensions;
|
||||||
mod iterators;
|
mod iterators;
|
||||||
|
mod layout;
|
||||||
mod text_format;
|
mod text_format;
|
||||||
|
|
||||||
pub use text_format::{FormatSpans, TextFormat};
|
pub use text_format::{FormatSpans, TextFormat};
|
||||||
|
|
|
@ -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<Twips>,
|
||||||
|
|
||||||
|
/// The resolved font object to use when measuring text.
|
||||||
|
font: Option<Font<'gc>>,
|
||||||
|
|
||||||
|
/// The start of the current chain of layout boxes.
|
||||||
|
first_box: Option<GcCell<'gc, LayoutBox<'gc>>>,
|
||||||
|
|
||||||
|
/// The end of the current chain of layout boxes.
|
||||||
|
last_box: Option<GcCell<'gc, LayoutBox<'gc>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Twips> {
|
||||||
|
&self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_mut(&mut self) -> &mut Position<Twips> {
|
||||||
|
&mut self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font(&self) -> Option<Font<'gc>> {
|
||||||
|
self.font
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_font(
|
||||||
|
&mut self,
|
||||||
|
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||||
|
movie: Arc<SwfMovie>,
|
||||||
|
name: &str,
|
||||||
|
) -> Option<Font<'gc>> {
|
||||||
|
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<GcCell<'gc, LayoutBox<'gc>>> {
|
||||||
|
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:
|
||||||
|
///
|
||||||
|
/// ```<p>I'm a <i>layout element</i> node!</p>```
|
||||||
|
///
|
||||||
|
/// produces a top-level `LayoutBox` for the `<p>` tag, which contains one or
|
||||||
|
/// more generated boxes for each run of text. The `<i>` 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<Twips>,
|
||||||
|
|
||||||
|
/// The layout box to be placed after this one.
|
||||||
|
next_sibling: Option<GcCell<'gc, LayoutBox<'gc>>>,
|
||||||
|
|
||||||
|
/// 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<GcCell<'gc, LayoutBox<'gc>>> {
|
||||||
|
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<SwfMovie>,
|
||||||
|
bounds: Twips,
|
||||||
|
) -> Option<GcCell<'gc, LayoutBox<'gc>>> {
|
||||||
|
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<Twips> {
|
||||||
|
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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue