mirror of
https://github.com/typst/typst
synced 2025-05-18 11:05:28 +08:00
Baseline alignment ⏏
This commit is contained in:
parent
c00cca3677
commit
8245b7b736
@ -10,14 +10,16 @@ use crate::geom::{Length, Path, Point, Size};
|
|||||||
pub struct Frame {
|
pub struct Frame {
|
||||||
/// The size of the frame.
|
/// The size of the frame.
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
|
/// The baseline of the frame measured from the top.
|
||||||
|
pub baseline: Length,
|
||||||
/// The elements composing this layout.
|
/// The elements composing this layout.
|
||||||
pub elements: Vec<(Point, Element)>,
|
pub elements: Vec<(Point, Element)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Frame {
|
impl Frame {
|
||||||
/// Create a new, empty frame.
|
/// Create a new, empty frame.
|
||||||
pub fn new(size: Size) -> Self {
|
pub fn new(size: Size, baseline: Length) -> Self {
|
||||||
Self { size, elements: vec![] }
|
Self { size, baseline, elements: vec![] }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an element at a position.
|
/// Add an element at a position.
|
||||||
|
@ -102,7 +102,9 @@ struct ParLayouter<'a> {
|
|||||||
|
|
||||||
struct Line {
|
struct Line {
|
||||||
items: Vec<LineItem>,
|
items: Vec<LineItem>,
|
||||||
size: Size,
|
width: Length,
|
||||||
|
top: Length,
|
||||||
|
bottom: Length,
|
||||||
ruler: Align,
|
ruler: Align,
|
||||||
hard: bool,
|
hard: bool,
|
||||||
}
|
}
|
||||||
@ -123,22 +125,17 @@ impl<'a> ParLayouter<'a> {
|
|||||||
finished: vec![],
|
finished: vec![],
|
||||||
stack: vec![],
|
stack: vec![],
|
||||||
stack_size: Size::ZERO,
|
stack_size: Size::ZERO,
|
||||||
line: Line {
|
line: Line::new(true),
|
||||||
items: vec![],
|
|
||||||
size: Size::ZERO,
|
|
||||||
ruler: Align::Start,
|
|
||||||
hard: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push horizontal spacing.
|
/// Push horizontal spacing.
|
||||||
fn push_spacing(&mut self, range: Range<usize>, amount: Length) {
|
fn push_spacing(&mut self, range: Range<usize>, amount: Length) {
|
||||||
let amount = amount.min(self.areas.current.width - self.line.size.width);
|
let amount = amount.min(self.areas.current.width - self.line.width);
|
||||||
self.line.size.width += amount;
|
self.line.width += amount;
|
||||||
self.line.items.push(LineItem {
|
self.line.items.push(LineItem {
|
||||||
range,
|
range,
|
||||||
frame: Frame::new(Size::new(amount, Length::ZERO)),
|
frame: Frame::new(Size::new(amount, Length::ZERO), Length::ZERO),
|
||||||
align: Align::default(),
|
align: Align::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -266,7 +263,7 @@ impl<'a> ParLayouter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find out whether the area still has enough space for this frame.
|
// Find out whether the area still has enough space for this frame.
|
||||||
if !self.usable().fits(frame.size) && self.line.size.width > Length::ZERO {
|
if !self.usable().fits(frame.size) && self.line.width > Length::ZERO {
|
||||||
self.finish_line(false);
|
self.finish_line(false);
|
||||||
|
|
||||||
// Here, we can directly check whether the frame fits into
|
// Here, we can directly check whether the frame fits into
|
||||||
@ -285,10 +282,11 @@ impl<'a> ParLayouter<'a> {
|
|||||||
|
|
||||||
// A line can contain frames with different alignments. Their exact
|
// A line can contain frames with different alignments. Their exact
|
||||||
// positions are calculated later depending on the alignments.
|
// positions are calculated later depending on the alignments.
|
||||||
let size = frame.size;
|
let Frame { size, baseline, .. } = frame;
|
||||||
self.line.items.push(LineItem { range, frame, align });
|
self.line.items.push(LineItem { range, frame, align });
|
||||||
self.line.size.width += size.width;
|
self.line.width += size.width;
|
||||||
self.line.size.height = self.line.size.height.max(size.height);
|
self.line.top = self.line.top.max(baseline);
|
||||||
|
self.line.bottom = self.line.bottom.max(size.height - baseline);
|
||||||
self.line.ruler = align;
|
self.line.ruler = align;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,41 +295,39 @@ impl<'a> ParLayouter<'a> {
|
|||||||
// `areas.current`, but the width of the current line needs to be
|
// `areas.current`, but the width of the current line needs to be
|
||||||
// subtracted to make sure the frame fits.
|
// subtracted to make sure the frame fits.
|
||||||
let mut usable = self.areas.current;
|
let mut usable = self.areas.current;
|
||||||
usable.width -= self.line.size.width;
|
usable.width -= self.line.width;
|
||||||
usable
|
usable
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_line(&mut self, hard: bool) {
|
fn finish_line(&mut self, hard: bool) {
|
||||||
if !mem::replace(&mut self.line.hard, hard) && self.line.items.is_empty() {
|
let mut line = mem::replace(&mut self.line, Line::new(hard));
|
||||||
|
if !line.hard && line.items.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BiDi reordering.
|
// BiDi reordering.
|
||||||
self.reorder_line();
|
line.reorder(&self.bidi);
|
||||||
|
|
||||||
let full_size = {
|
let full_size = {
|
||||||
let expand = self.areas.expand.horizontal;
|
let expand = self.areas.expand.horizontal;
|
||||||
let full = self.areas.full.width;
|
let full = self.areas.full.width;
|
||||||
Size::new(
|
Size::new(expand.resolve(line.width, full), line.top + line.bottom)
|
||||||
expand.resolve(self.line.size.width, full),
|
|
||||||
self.line.size.height,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut output = Frame::new(full_size);
|
let mut output = Frame::new(full_size, line.top + line.bottom);
|
||||||
let mut offset = Length::ZERO;
|
let mut offset = Length::ZERO;
|
||||||
|
|
||||||
for item in mem::take(&mut self.line.items) {
|
for item in line.items {
|
||||||
// Align along the x axis.
|
// Align along the x axis.
|
||||||
let x = item.align.resolve(if self.dir.is_positive() {
|
let x = item.align.resolve(if self.dir.is_positive() {
|
||||||
offset .. full_size.width - self.line.size.width + offset
|
offset .. full_size.width - line.width + offset
|
||||||
} else {
|
} else {
|
||||||
full_size.width - self.line.size.width + offset .. offset
|
full_size.width - line.width + offset .. offset
|
||||||
});
|
});
|
||||||
|
|
||||||
offset += item.frame.size.width;
|
offset += item.frame.size.width;
|
||||||
|
|
||||||
let pos = Point::new(x, Length::ZERO);
|
let pos = Point::new(x, line.top - item.frame.baseline);
|
||||||
output.push_frame(pos, item.frame);
|
output.push_frame(pos, item.frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,31 +337,73 @@ impl<'a> ParLayouter<'a> {
|
|||||||
self.areas.current.height -= self.line_spacing;
|
self.areas.current.height -= self.line_spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.stack.push((self.stack_size.height, output, self.line.ruler));
|
self.stack.push((self.stack_size.height, output, line.ruler));
|
||||||
self.stack_size.height += full_size.height;
|
self.stack_size.height += full_size.height;
|
||||||
self.stack_size.width = self.stack_size.width.max(full_size.width);
|
self.stack_size.width = self.stack_size.width.max(full_size.width);
|
||||||
self.areas.current.height -= full_size.height;
|
self.areas.current.height -= full_size.height;
|
||||||
self.line.size = Size::ZERO;
|
|
||||||
self.line.ruler = Align::Start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reorder_line(&mut self) {
|
fn finish_area(&mut self) {
|
||||||
let items = &mut self.line.items;
|
let mut output = Frame::new(self.stack_size, Length::ZERO);
|
||||||
|
let mut baseline = None;
|
||||||
|
|
||||||
|
for (before, line, align) in mem::take(&mut self.stack) {
|
||||||
|
// Align along the x axis.
|
||||||
|
let x = align.resolve(if self.dir.is_positive() {
|
||||||
|
Length::ZERO .. self.stack_size.width - line.size.width
|
||||||
|
} else {
|
||||||
|
self.stack_size.width - line.size.width .. Length::ZERO
|
||||||
|
});
|
||||||
|
|
||||||
|
let pos = Point::new(x, before);
|
||||||
|
baseline.get_or_insert(pos.y + line.baseline);
|
||||||
|
output.push_frame(pos, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(baseline) = baseline {
|
||||||
|
output.baseline = baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.finished.push(output);
|
||||||
|
self.areas.next();
|
||||||
|
self.stack_size = Size::ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(mut self) -> Vec<Frame> {
|
||||||
|
self.finish_line(false);
|
||||||
|
self.finish_area();
|
||||||
|
self.finished
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
fn new(hard: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
items: vec![],
|
||||||
|
width: Length::ZERO,
|
||||||
|
top: Length::ZERO,
|
||||||
|
bottom: Length::ZERO,
|
||||||
|
ruler: Align::Start,
|
||||||
|
hard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reorder(&mut self, bidi: &BidiInfo) {
|
||||||
|
let items = &mut self.items;
|
||||||
let line_range = match (items.first(), items.last()) {
|
let line_range = match (items.first(), items.last()) {
|
||||||
(Some(first), Some(last)) => first.range.start .. last.range.end,
|
(Some(first), Some(last)) => first.range.start .. last.range.end,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the paragraph that contains the frame.
|
// Find the paragraph that contains the frame.
|
||||||
let para = self
|
let para = bidi
|
||||||
.bidi
|
|
||||||
.paragraphs
|
.paragraphs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|para| para.range.contains(&line_range.start))
|
.find(|para| para.range.contains(&line_range.start))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Compute the reordered ranges in visual order (left to right).
|
// Compute the reordered ranges in visual order (left to right).
|
||||||
let (levels, ranges) = self.bidi.visual_runs(para, line_range);
|
let (levels, ranges) = bidi.visual_runs(para, line_range);
|
||||||
|
|
||||||
// Reorder the items.
|
// Reorder the items.
|
||||||
items.sort_by_key(|item| {
|
items.sort_by_key(|item| {
|
||||||
@ -387,31 +425,6 @@ impl<'a> ParLayouter<'a> {
|
|||||||
(idx, dist)
|
(idx, dist)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_area(&mut self) {
|
|
||||||
let mut output = Frame::new(self.stack_size);
|
|
||||||
for (before, line, align) in mem::take(&mut self.stack) {
|
|
||||||
// Align along the x axis.
|
|
||||||
let x = align.resolve(if self.dir.is_positive() {
|
|
||||||
Length::ZERO .. self.stack_size.width - line.size.width
|
|
||||||
} else {
|
|
||||||
self.stack_size.width - line.size.width .. Length::ZERO
|
|
||||||
});
|
|
||||||
|
|
||||||
let pos = Point::new(x, before);
|
|
||||||
output.push_frame(pos, line);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.finished.push(output);
|
|
||||||
self.areas.next();
|
|
||||||
self.stack_size = Size::ZERO;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(mut self) -> Vec<Frame> {
|
|
||||||
self.finish_line(false);
|
|
||||||
self.finish_area();
|
|
||||||
self.finished
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trait LevelExt: Sized {
|
trait LevelExt: Sized {
|
||||||
|
@ -5,19 +5,34 @@ use ttf_parser::GlyphId;
|
|||||||
use super::{Element, Frame, ShapedText};
|
use super::{Element, Frame, ShapedText};
|
||||||
use crate::env::FontLoader;
|
use crate::env::FontLoader;
|
||||||
use crate::exec::FontProps;
|
use crate::exec::FontProps;
|
||||||
use crate::geom::{Dir, Point, Size};
|
use crate::geom::{Dir, Length, Point, Size};
|
||||||
|
|
||||||
/// Shape text into a frame containing [`ShapedText`] runs.
|
/// Shape text into a frame containing [`ShapedText`] runs.
|
||||||
pub fn shape(text: &str, dir: Dir, loader: &mut FontLoader, props: &FontProps) -> Frame {
|
pub fn shape(text: &str, dir: Dir, loader: &mut FontLoader, props: &FontProps) -> Frame {
|
||||||
let mut frame = Frame::new(Size::ZERO);
|
|
||||||
let iter = props.families.iter();
|
let iter = props.families.iter();
|
||||||
shape_segment(&mut frame, text, dir, loader, props, iter, None);
|
let mut results = vec![];
|
||||||
|
shape_segment(&mut results, text, dir, loader, props, iter, None);
|
||||||
|
|
||||||
|
let mut top = Length::ZERO;
|
||||||
|
let mut bottom = Length::ZERO;
|
||||||
|
for result in &results {
|
||||||
|
top = top.max(result.top);
|
||||||
|
bottom = bottom.max(result.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut frame = Frame::new(Size::new(Length::ZERO, top + bottom), top);
|
||||||
|
for shaped in results {
|
||||||
|
let offset = frame.size.width;
|
||||||
|
frame.size.width += shaped.width;
|
||||||
|
frame.push(Point::new(offset, top), Element::Text(shaped));
|
||||||
|
}
|
||||||
|
|
||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shape text into a frame with font fallback using the `families` iterator.
|
/// Shape text into a frame with font fallback using the `families` iterator.
|
||||||
fn shape_segment<'a>(
|
fn shape_segment<'a>(
|
||||||
frame: &mut Frame,
|
results: &mut Vec<ShapedText>,
|
||||||
text: &str,
|
text: &str,
|
||||||
dir: Dir,
|
dir: Dir,
|
||||||
loader: &mut FontLoader,
|
loader: &mut FontLoader,
|
||||||
@ -53,7 +68,7 @@ fn shape_segment<'a>(
|
|||||||
let units_per_em = f64::from(ttf.units_per_em().unwrap_or(1000));
|
let units_per_em = f64::from(ttf.units_per_em().unwrap_or(1000));
|
||||||
let convert = |units| f64::from(units) / units_per_em * props.size;
|
let convert = |units| f64::from(units) / units_per_em * props.size;
|
||||||
let top = convert(i32::from(props.top_edge.lookup(ttf)));
|
let top = convert(i32::from(props.top_edge.lookup(ttf)));
|
||||||
let bottom = convert(i32::from(props.bottom_edge.lookup(ttf)));
|
let bottom = convert(i32::from(-props.bottom_edge.lookup(ttf)));
|
||||||
let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
|
let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
|
||||||
|
|
||||||
// Fill the buffer with our text.
|
// Fill the buffer with our text.
|
||||||
@ -76,7 +91,7 @@ fn shape_segment<'a>(
|
|||||||
if info.codepoint == 0 && fallback {
|
if info.codepoint == 0 && fallback {
|
||||||
// Flush what we have so far.
|
// Flush what we have so far.
|
||||||
if !shaped.glyphs.is_empty() {
|
if !shaped.glyphs.is_empty() {
|
||||||
place(frame, shaped);
|
results.push(shaped);
|
||||||
shaped = ShapedText::new(id, props.size, top, bottom, props.color);
|
shaped = ShapedText::new(id, props.size, top, bottom, props.color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +122,7 @@ fn shape_segment<'a>(
|
|||||||
let part = &text[range];
|
let part = &text[range];
|
||||||
|
|
||||||
// Recursively shape the tofu sequence with the next family.
|
// Recursively shape the tofu sequence with the next family.
|
||||||
shape_segment(frame, part, dir, loader, props, families.clone(), first);
|
shape_segment(results, part, dir, loader, props, families.clone(), first);
|
||||||
} else {
|
} else {
|
||||||
// Add the glyph to the shaped output.
|
// Add the glyph to the shaped output.
|
||||||
// TODO: Don't ignore y_advance and y_offset.
|
// TODO: Don't ignore y_advance and y_offset.
|
||||||
@ -119,14 +134,6 @@ fn shape_segment<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !shaped.glyphs.is_empty() {
|
if !shaped.glyphs.is_empty() {
|
||||||
place(frame, shaped)
|
results.push(shaped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Place shaped text into a frame.
|
|
||||||
fn place(frame: &mut Frame, shaped: ShapedText) {
|
|
||||||
let offset = frame.size.width;
|
|
||||||
frame.size.width += shaped.width;
|
|
||||||
frame.size.height = frame.size.height.max(shaped.top - shaped.bottom);
|
|
||||||
frame.push(Point::new(offset, shaped.top), Element::Text(shaped));
|
|
||||||
}
|
|
||||||
|
@ -119,7 +119,8 @@ impl StackLayouter {
|
|||||||
size.switch(self.main)
|
size.switch(self.main)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut output = Frame::new(full_size.switch(self.main).to_size());
|
let mut output = Frame::new(full_size.switch(self.main).to_size(), Length::ZERO);
|
||||||
|
let mut baseline = None;
|
||||||
|
|
||||||
for (before, frame, aligns) in std::mem::take(&mut self.frames) {
|
for (before, frame, aligns) in std::mem::take(&mut self.frames) {
|
||||||
let child_size = frame.size.switch(self.main);
|
let child_size = frame.size.switch(self.main);
|
||||||
@ -142,9 +143,14 @@ impl StackLayouter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let pos = Gen::new(main, cross).switch(self.main).to_point();
|
let pos = Gen::new(main, cross).switch(self.main).to_point();
|
||||||
|
baseline.get_or_insert(pos.y + frame.baseline);
|
||||||
output.push_frame(pos, frame);
|
output.push_frame(pos, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(baseline) = baseline {
|
||||||
|
output.baseline = baseline;
|
||||||
|
}
|
||||||
|
|
||||||
self.finished.push(output);
|
self.finished.push(output);
|
||||||
self.areas.next();
|
self.areas.next();
|
||||||
self.ruler = Align::Start;
|
self.ruler = Align::Start;
|
||||||
|
@ -73,7 +73,7 @@ impl Layout for ImageNode {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut frame = Frame::new(size);
|
let mut frame = Frame::new(size, size.height);
|
||||||
frame.push(Point::ZERO, Element::Image(Image { res: self.res, size }));
|
frame.push(Point::ZERO, Element::Image(Image { res: self.res, size }));
|
||||||
|
|
||||||
vec![frame]
|
vec![frame]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user