Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Martin 2021-08-23 23:56:33 +02:00 committed by GitHub
parent 0806af4aec
commit d546453880
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 196 additions and 73 deletions

View File

@ -20,7 +20,7 @@ pub struct Template(Rc<Vec<TemplateNode>>);
#[derive(Clone)] #[derive(Clone)]
enum TemplateNode { enum TemplateNode {
/// A word space. /// A word space.
Space, Space(Vec<Decoration>),
/// A line break. /// A line break.
Linebreak, Linebreak,
/// A paragraph break. /// A paragraph break.
@ -28,11 +28,11 @@ enum TemplateNode {
/// A page break. /// A page break.
Pagebreak(bool), Pagebreak(bool),
/// Plain text. /// Plain text.
Text(EcoString), Text(EcoString, Vec<Decoration>),
/// Spacing. /// Spacing.
Spacing(GenAxis, Linear), Spacing(GenAxis, Linear),
/// An inline node builder. /// An inline node builder.
Inline(Rc<dyn Fn(&State) -> LayoutNode>), Inline(Rc<dyn Fn(&State) -> LayoutNode>, Vec<Decoration>),
/// An block node builder. /// An block node builder.
Block(Rc<dyn Fn(&State) -> LayoutNode>), Block(Rc<dyn Fn(&State) -> LayoutNode>),
/// Save the current state. /// Save the current state.
@ -43,6 +43,13 @@ enum TemplateNode {
Modify(Rc<dyn Fn(&mut State)>), Modify(Rc<dyn Fn(&mut State)>),
} }
/// A template node decoration.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Decoration {
/// A link.
Link(EcoString),
}
impl Template { impl Template {
/// Create a new, empty template. /// Create a new, empty template.
pub fn new() -> Self { pub fn new() -> Self {
@ -55,7 +62,7 @@ impl Template {
F: Fn(&State) -> T + 'static, F: Fn(&State) -> T + 'static,
T: Into<LayoutNode>, T: Into<LayoutNode>,
{ {
let node = TemplateNode::Inline(Rc::new(move |s| f(s).into())); let node = TemplateNode::Inline(Rc::new(move |s| f(s).into()), vec![]);
Self(Rc::new(vec![node])) Self(Rc::new(vec![node]))
} }
@ -71,7 +78,7 @@ impl Template {
/// Add a word space to the template. /// Add a word space to the template.
pub fn space(&mut self) { pub fn space(&mut self) {
self.make_mut().push(TemplateNode::Space); self.make_mut().push(TemplateNode::Space(vec![]));
} }
/// Add a line break to the template. /// Add a line break to the template.
@ -91,7 +98,7 @@ impl Template {
/// Add text to the template. /// Add text to the template.
pub fn text(&mut self, text: impl Into<EcoString>) { pub fn text(&mut self, text: impl Into<EcoString>) {
self.make_mut().push(TemplateNode::Text(text.into())); self.make_mut().push(TemplateNode::Text(text.into(), vec![]));
} }
/// Add text, but in monospace. /// Add text, but in monospace.
@ -107,6 +114,19 @@ impl Template {
self.make_mut().push(TemplateNode::Spacing(axis, spacing)); self.make_mut().push(TemplateNode::Spacing(axis, spacing));
} }
/// Add a decoration to the last template node.
pub fn decorate(&mut self, deco: Decoration) {
for node in self.make_mut() {
let decos = match node {
TemplateNode::Space(decos) => decos,
TemplateNode::Text(_, decos) => decos,
TemplateNode::Inline(_, decos) => decos,
_ => continue,
};
decos.push(deco.clone());
}
}
/// Register a restorable snapshot. /// Register a restorable snapshot.
pub fn save(&mut self) { pub fn save(&mut self) {
self.make_mut().push(TemplateNode::Save); self.make_mut().push(TemplateNode::Save);
@ -201,7 +221,7 @@ impl Add<Str> for Template {
type Output = Self; type Output = Self;
fn add(mut self, rhs: Str) -> Self::Output { fn add(mut self, rhs: Str) -> Self::Output {
Rc::make_mut(&mut self.0).push(TemplateNode::Text(rhs.into())); Rc::make_mut(&mut self.0).push(TemplateNode::Text(rhs.into(), vec![]));
self self
} }
} }
@ -210,7 +230,7 @@ impl Add<Template> for Str {
type Output = Template; type Output = Template;
fn add(self, mut rhs: Template) -> Self::Output { fn add(self, mut rhs: Template) -> Self::Output {
Rc::make_mut(&mut rhs.0).insert(0, TemplateNode::Text(self.into())); Rc::make_mut(&mut rhs.0).insert(0, TemplateNode::Text(self.into(), vec![]));
rhs rhs
} }
} }
@ -261,26 +281,26 @@ impl Builder {
self.pagebreak(true, false); self.pagebreak(true, false);
} }
} }
TemplateNode::Space => self.space(), TemplateNode::Space(decos) => self.space(decos),
TemplateNode::Linebreak => self.linebreak(), TemplateNode::Linebreak => self.linebreak(),
TemplateNode::Parbreak => self.parbreak(), TemplateNode::Parbreak => self.parbreak(),
TemplateNode::Pagebreak(keep) => self.pagebreak(*keep, true), TemplateNode::Pagebreak(keep) => self.pagebreak(*keep, true),
TemplateNode::Text(text) => self.text(text), TemplateNode::Text(text, decos) => self.text(text, decos),
TemplateNode::Spacing(axis, amount) => self.spacing(*axis, *amount), TemplateNode::Spacing(axis, amount) => self.spacing(*axis, *amount),
TemplateNode::Inline(f) => self.inline(f(&self.state)), TemplateNode::Inline(f, decos) => self.inline(f(&self.state), decos),
TemplateNode::Block(f) => self.block(f(&self.state)), TemplateNode::Block(f) => self.block(f(&self.state)),
TemplateNode::Modify(f) => f(&mut self.state), TemplateNode::Modify(f) => f(&mut self.state),
} }
} }
/// Push a word space into the active paragraph. /// Push a word space into the active paragraph.
fn space(&mut self) { fn space(&mut self, decos: &[Decoration]) {
self.stack.par.push_soft(self.make_text_node(' ')); self.stack.par.push_soft(self.make_text_node(' ', decos.to_vec()));
} }
/// Apply a forced line break. /// Apply a forced line break.
fn linebreak(&mut self) { fn linebreak(&mut self) {
self.stack.par.push_hard(self.make_text_node('\n')); self.stack.par.push_hard(self.make_text_node('\n', vec![]));
} }
/// Apply a forced paragraph break. /// Apply a forced paragraph break.
@ -300,16 +320,14 @@ impl Builder {
} }
/// Push text into the active paragraph. /// Push text into the active paragraph.
/// fn text(&mut self, text: impl Into<EcoString>, decos: &[Decoration]) {
/// The text is split into lines at newlines. self.stack.par.push(self.make_text_node(text, decos.to_vec()));
fn text(&mut self, text: impl Into<EcoString>) {
self.stack.par.push(self.make_text_node(text));
} }
/// Push an inline node into the active paragraph. /// Push an inline node into the active paragraph.
fn inline(&mut self, node: impl Into<LayoutNode>) { fn inline(&mut self, node: impl Into<LayoutNode>, decos: &[Decoration]) {
let align = self.state.aligns.inline; let align = self.state.aligns.inline;
self.stack.par.push(ParChild::Any(node.into(), align)); self.stack.par.push(ParChild::Any(node.into(), align, decos.to_vec()));
} }
/// Push a block node into the active stack, finishing the active paragraph. /// Push a block node into the active stack, finishing the active paragraph.
@ -348,11 +366,16 @@ impl Builder {
/// Construct a text node with the given text and settings from the active /// Construct a text node with the given text and settings from the active
/// state. /// state.
fn make_text_node(&self, text: impl Into<EcoString>) -> ParChild { fn make_text_node(
&self,
text: impl Into<EcoString>,
decos: Vec<Decoration>,
) -> ParChild {
ParChild::Text( ParChild::Text(
text.into(), text.into(),
self.state.aligns.inline, self.state.aligns.inline,
Rc::clone(&self.state.font), Rc::clone(&self.state.font),
decos,
) )
} }
} }
@ -465,11 +488,14 @@ impl ParBuilder {
} }
fn push_inner(&mut self, child: ParChild) { fn push_inner(&mut self, child: ParChild) {
if let ParChild::Text(curr_text, curr_align, curr_props) = &child { if let ParChild::Text(curr_text, curr_align, curr_props, curr_decos) = &child {
if let Some(ParChild::Text(prev_text, prev_align, prev_props)) = if let Some(ParChild::Text(prev_text, prev_align, prev_props, prev_decos)) =
self.children.last_mut() self.children.last_mut()
{ {
if prev_align == curr_align && Rc::ptr_eq(prev_props, curr_props) { if prev_align == curr_align
&& Rc::ptr_eq(prev_props, curr_props)
&& curr_decos == prev_decos
{
prev_text.push_str(&curr_text); prev_text.push_str(&curr_text);
return; return;
} }

View File

@ -115,6 +115,7 @@ fn walk_item(ctx: &mut EvalContext, label: EcoString, body: Template) {
label.clone(), label.clone(),
state.aligns.inline, state.aligns.inline,
Rc::clone(&state.font), Rc::clone(&state.font),
vec![],
)], )],
}; };
StackNode { StackNode {

View File

@ -8,8 +8,8 @@ use std::rc::Rc;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba}; use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
use miniz_oxide::deflate; use miniz_oxide::deflate;
use pdf_writer::{ use pdf_writer::{
CidFontType, ColorSpace, Content, Filter, FontFlags, Name, PdfWriter, Rect, Ref, Str, ActionType, AnnotationType, CidFontType, ColorSpace, Content, Filter, FontFlags,
SystemInfo, UnicodeCmap, Name, PdfWriter, Rect, Ref, Str, SystemInfo, UnicodeCmap,
}; };
use ttf_parser::{name_id, GlyphId}; use ttf_parser::{name_id, GlyphId};
@ -59,6 +59,7 @@ impl<'a> PdfExporter<'a> {
} }
image_map.insert(id); image_map.insert(id);
} }
Element::Link(_, _) => {}
} }
} }
} }
@ -116,16 +117,34 @@ impl<'a> PdfExporter<'a> {
for ((page_id, content_id), page) in for ((page_id, content_id), page) in
self.refs.pages().zip(self.refs.contents()).zip(self.frames) self.refs.pages().zip(self.refs.contents()).zip(self.frames)
{ {
self.writer let w = page.size.w.to_pt() as f32;
.page(page_id) let h = page.size.h.to_pt() as f32;
let mut page_writer = self.writer.page(page_id);
page_writer
.parent(self.refs.page_tree) .parent(self.refs.page_tree)
.media_box(Rect::new( .media_box(Rect::new(0.0, 0.0, w, h));
0.0,
0.0, let mut annotations = page_writer.annotations();
page.size.w.to_pt() as f32, for (pos, element) in page.elements() {
page.size.h.to_pt() as f32, if let Element::Link(href, size) = element {
)) let x = pos.x.to_pt() as f32;
.contents(content_id); let y = (page.size.h - pos.y).to_pt() as f32;
let w = size.w.to_pt() as f32;
let h = size.h.to_pt() as f32;
annotations
.push()
.subtype(AnnotationType::Link)
.rect(Rect::new(x, y - h, x + w, y))
.action()
.action_type(ActionType::Uri)
.uri(Str(href.as_bytes()));
}
}
drop(annotations);
page_writer.contents(content_id);
} }
} }
@ -248,6 +267,8 @@ impl<'a> PdfExporter<'a> {
content.x_object(Name(name.as_bytes())); content.x_object(Name(name.as_bytes()));
content.restore_state(); content.restore_state();
} }
Element::Link(_, _) => {}
} }
} }

View File

@ -16,17 +16,17 @@ pub struct Frame {
/// The baseline of the frame measured from the top. /// The baseline of the frame measured from the top.
pub baseline: Length, pub baseline: Length,
/// The elements composing this layout. /// The elements composing this layout.
children: Vec<(Point, Child)>, pub children: Vec<(Point, FrameChild)>,
} }
/// A frame can contain two different kinds of children: a leaf element or a /// A frame can contain two different kinds of children: a leaf element or a
/// nested frame. /// nested frame.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
enum Child { pub enum FrameChild {
/// A leaf node in the frame tree. /// A leaf node in the frame tree.
Element(Element), Element(Element),
/// An interior node. /// An interior node with an optional index.
Frame(Rc<Frame>), Frame(Option<usize>, Rc<Frame>),
} }
impl Frame { impl Frame {
@ -38,17 +38,22 @@ impl Frame {
/// Add an element at a position in the foreground. /// Add an element at a position in the foreground.
pub fn push(&mut self, pos: Point, element: Element) { pub fn push(&mut self, pos: Point, element: Element) {
self.children.push((pos, Child::Element(element))); self.children.push((pos, FrameChild::Element(element)));
} }
/// Add an element at a position in the background. /// Add an element at a position in the background.
pub fn prepend(&mut self, pos: Point, element: Element) { pub fn prepend(&mut self, pos: Point, element: Element) {
self.children.insert(0, (pos, Child::Element(element))) self.children.insert(0, (pos, FrameChild::Element(element)));
} }
/// Add a frame element. /// Add a frame element.
pub fn push_frame(&mut self, pos: Point, subframe: Rc<Self>) { pub fn push_frame(&mut self, pos: Point, subframe: Rc<Self>) {
self.children.push((pos, Child::Frame(subframe))) self.children.push((pos, FrameChild::Frame(None, subframe)))
}
/// Add a frame element with an index of arbitrary use.
pub fn push_indexed_frame(&mut self, pos: Point, index: usize, subframe: Rc<Self>) {
self.children.push((pos, FrameChild::Frame(Some(index), subframe)));
} }
/// Add all elements of another frame, placing them relative to the given /// Add all elements of another frame, placing them relative to the given
@ -85,12 +90,12 @@ impl<'a> Iterator for Elements<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let (cursor, offset, frame) = self.stack.last_mut()?; let (cursor, offset, frame) = self.stack.last_mut()?;
match frame.children.get(*cursor) { match frame.children.get(*cursor) {
Some((pos, Child::Frame(f))) => { Some((pos, FrameChild::Frame(_, f))) => {
let new_offset = *offset + *pos; let new_offset = *offset + *pos;
self.stack.push((0, new_offset, f.as_ref())); self.stack.push((0, new_offset, f.as_ref()));
self.next() self.next()
} }
Some((pos, Child::Element(e))) => { Some((pos, FrameChild::Element(e))) => {
*cursor += 1; *cursor += 1;
Some((*offset + *pos, e)) Some((*offset + *pos, e))
} }
@ -115,6 +120,8 @@ pub enum Element {
Geometry(Geometry, Paint), Geometry(Geometry, Paint),
/// A raster image. /// A raster image.
Image(ImageId, Size), Image(ImageId, Size),
/// A link to an external resource.
Link(String, Size),
} }
/// A run of shaped text. /// A run of shaped text.

View File

@ -4,7 +4,7 @@ use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator; use xi_unicode::LineBreakIterator;
use super::*; use super::*;
use crate::eval::FontState; use crate::eval::{Decoration, FontState};
use crate::util::{EcoString, RangeExt, SliceExt}; use crate::util::{EcoString, RangeExt, SliceExt};
type Range = std::ops::Range<usize>; type Range = std::ops::Range<usize>;
@ -26,9 +26,9 @@ pub enum ParChild {
/// Spacing between other nodes. /// Spacing between other nodes.
Spacing(Linear), Spacing(Linear),
/// A run of text and how to align it in its line. /// A run of text and how to align it in its line.
Text(EcoString, Align, Rc<FontState>), Text(EcoString, Align, Rc<FontState>, Vec<Decoration>),
/// Any child node and how to align it in its line. /// Any child node and how to align it in its line.
Any(LayoutNode, Align), Any(LayoutNode, Align, Vec<Decoration>),
} }
impl Layout for ParNode { impl Layout for ParNode {
@ -48,7 +48,7 @@ impl Layout for ParNode {
let layouter = ParLayouter::new(self, ctx, regions, bidi); let layouter = ParLayouter::new(self, ctx, regions, bidi);
// Find suitable linebreaks. // Find suitable linebreaks.
layouter.layout(ctx, regions.clone()) layouter.layout(ctx, &self.children, regions.clone())
} }
} }
@ -79,8 +79,8 @@ impl ParNode {
fn strings(&self) -> impl Iterator<Item = &str> { fn strings(&self) -> impl Iterator<Item = &str> {
self.children.iter().map(|child| match child { self.children.iter().map(|child| match child {
ParChild::Spacing(_) => " ", ParChild::Spacing(_) => " ",
ParChild::Text(ref piece, _, _) => piece, ParChild::Text(ref piece, ..) => piece,
ParChild::Any(_, _) => "\u{FFFC}", ParChild::Any(..) => "\u{FFFC}",
}) })
} }
} }
@ -119,25 +119,25 @@ impl<'a> ParLayouter<'a> {
let mut ranges = vec![]; let mut ranges = vec![];
// Layout the children and collect them into items. // Layout the children and collect them into items.
for (range, child) in par.ranges().zip(&par.children) { for (i, (range, child)) in par.ranges().zip(&par.children).enumerate() {
match *child { match *child {
ParChild::Spacing(amount) => { ParChild::Spacing(amount) => {
let resolved = amount.resolve(regions.current.w); let resolved = amount.resolve(regions.current.w);
items.push(ParItem::Spacing(resolved)); items.push(ParItem::Spacing(resolved));
ranges.push(range); ranges.push(range);
} }
ParChild::Text(_, align, ref state) => { ParChild::Text(_, align, ref state, _) => {
// TODO: Also split by language and script. // TODO: Also split by language and script.
for (subrange, dir) in split_runs(&bidi, range) { for (subrange, dir) in split_runs(&bidi, range) {
let text = &bidi.text[subrange.clone()]; let text = &bidi.text[subrange.clone()];
let shaped = shape(ctx, text, dir, state); let shaped = shape(ctx, text, dir, state);
items.push(ParItem::Text(shaped, align)); items.push(ParItem::Text(shaped, align, i));
ranges.push(subrange); ranges.push(subrange);
} }
} }
ParChild::Any(ref node, align) => { ParChild::Any(ref node, align, _) => {
let frame = node.layout(ctx, regions).remove(0); let frame = node.layout(ctx, regions).remove(0);
items.push(ParItem::Frame(frame.item, align)); items.push(ParItem::Frame(frame.item, align, i));
ranges.push(range); ranges.push(range);
} }
} }
@ -156,9 +156,10 @@ impl<'a> ParLayouter<'a> {
fn layout( fn layout(
self, self,
ctx: &mut LayoutContext, ctx: &mut LayoutContext,
children: &[ParChild],
regions: Regions, regions: Regions,
) -> Vec<Constrained<Rc<Frame>>> { ) -> Vec<Constrained<Rc<Frame>>> {
let mut stack = LineStack::new(self.line_spacing, regions); let mut stack = LineStack::new(self.line_spacing, children, regions);
// The current line attempt. // The current line attempt.
// Invariant: Always fits into `stack.regions.current`. // Invariant: Always fits into `stack.regions.current`.
@ -273,9 +274,9 @@ enum ParItem<'a> {
/// Spacing between other items. /// Spacing between other items.
Spacing(Length), Spacing(Length),
/// A shaped text run with consistent direction. /// A shaped text run with consistent direction.
Text(ShapedText<'a>, Align), Text(ShapedText<'a>, Align, usize),
/// A layouted child node. /// A layouted child node.
Frame(Rc<Frame>, Align), Frame(Rc<Frame>, Align, usize),
} }
impl ParItem<'_> { impl ParItem<'_> {
@ -283,8 +284,8 @@ impl ParItem<'_> {
pub fn size(&self) -> Size { pub fn size(&self) -> Size {
match self { match self {
Self::Spacing(amount) => Size::new(*amount, Length::zero()), Self::Spacing(amount) => Size::new(*amount, Length::zero()),
Self::Text(shaped, _) => shaped.size, Self::Text(shaped, ..) => shaped.size,
Self::Frame(frame, _) => frame.size, Self::Frame(frame, ..) => frame.size,
} }
} }
@ -292,8 +293,17 @@ impl ParItem<'_> {
pub fn baseline(&self) -> Length { pub fn baseline(&self) -> Length {
match self { match self {
Self::Spacing(_) => Length::zero(), Self::Spacing(_) => Length::zero(),
Self::Text(shaped, _) => shaped.baseline, Self::Text(shaped, ..) => shaped.baseline,
Self::Frame(frame, _) => frame.baseline, Self::Frame(frame, ..) => frame.baseline,
}
}
/// The index of the `ParChild` that this item belongs to.
pub fn index(&self) -> Option<usize> {
match *self {
Self::Spacing(_) => None,
Self::Text(.., index) => Some(index),
Self::Frame(.., index) => Some(index),
} }
} }
} }
@ -301,6 +311,7 @@ impl ParItem<'_> {
/// Stacks lines on top of each other. /// Stacks lines on top of each other.
struct LineStack<'a> { struct LineStack<'a> {
line_spacing: Length, line_spacing: Length,
children: &'a [ParChild],
full: Size, full: Size,
regions: Regions, regions: Regions,
size: Size, size: Size,
@ -312,11 +323,12 @@ struct LineStack<'a> {
impl<'a> LineStack<'a> { impl<'a> LineStack<'a> {
/// Create an empty line stack. /// Create an empty line stack.
fn new(line_spacing: Length, regions: Regions) -> Self { fn new(line_spacing: Length, children: &'a [ParChild], regions: Regions) -> Self {
Self { Self {
line_spacing, line_spacing,
constraints: Constraints::new(regions.expand), children,
full: regions.current, full: regions.current,
constraints: Constraints::new(regions.expand),
regions, regions,
size: Size::zero(), size: Size::zero(),
lines: vec![], lines: vec![],
@ -368,6 +380,25 @@ impl<'a> LineStack<'a> {
output.merge_frame(pos, frame); output.merge_frame(pos, frame);
} }
// For each frame, we look if any decorations apply.
for i in 0 .. output.children.len() {
let &(point, ref child) = &output.children[i];
if let &FrameChild::Frame(Some(frame_idx), ref frame) = child {
let size = frame.size;
for deco in match &self.children[frame_idx] {
ParChild::Spacing(_) => continue,
ParChild::Text(.., decos) => decos,
ParChild::Any(.., decos) => decos,
} {
match deco {
Decoration::Link(href) => {
output.push(point, Element::Link(href.to_string(), size));
}
}
}
}
}
self.finished.push(output.constrain(self.constraints)); self.finished.push(output.constrain(self.constraints));
self.regions.next(); self.regions.next();
self.full = self.regions.current; self.full = self.regions.current;
@ -426,7 +457,7 @@ impl<'a> LineLayout<'a> {
// Reshape the last item if it's split in half. // Reshape the last item if it's split in half.
let mut last = None; let mut last = None;
if let Some((ParItem::Text(shaped, align), rest)) = items.split_last() { if let Some((ParItem::Text(shaped, align, i), rest)) = items.split_last() {
// Compute the range we want to shape, trimming whitespace at the // Compute the range we want to shape, trimming whitespace at the
// end of the line. // end of the line.
let base = par.ranges[last_idx].start; let base = par.ranges[last_idx].start;
@ -442,7 +473,7 @@ impl<'a> LineLayout<'a> {
if !range.is_empty() || rest.is_empty() { if !range.is_empty() || rest.is_empty() {
// Reshape that part. // Reshape that part.
let reshaped = shaped.reshape(ctx, range); let reshaped = shaped.reshape(ctx, range);
last = Some(ParItem::Text(reshaped, *align)); last = Some(ParItem::Text(reshaped, *align, *i));
} }
items = rest; items = rest;
@ -452,7 +483,7 @@ impl<'a> LineLayout<'a> {
// Reshape the start item if it's split in half. // Reshape the start item if it's split in half.
let mut first = None; let mut first = None;
if let Some((ParItem::Text(shaped, align), rest)) = items.split_first() { if let Some((ParItem::Text(shaped, align, i), rest)) = items.split_first() {
// Compute the range we want to shape. // Compute the range we want to shape.
let Range { start: base, end: first_end } = par.ranges[first_idx]; let Range { start: base, end: first_end } = par.ranges[first_idx];
let start = line.start; let start = line.start;
@ -463,7 +494,7 @@ impl<'a> LineLayout<'a> {
if range.len() < shaped.text.len() { if range.len() < shaped.text.len() {
if !range.is_empty() { if !range.is_empty() {
let reshaped = shaped.reshape(ctx, range); let reshaped = shaped.reshape(ctx, range);
first = Some(ParItem::Text(reshaped, *align)); first = Some(ParItem::Text(reshaped, *align, *i));
} }
items = rest; items = rest;
@ -511,11 +542,11 @@ impl<'a> LineLayout<'a> {
offset += amount; offset += amount;
return; return;
} }
ParItem::Text(ref shaped, align) => { ParItem::Text(ref shaped, align, _) => {
ruler = ruler.max(align); ruler = ruler.max(align);
Rc::new(shaped.build(ctx)) Rc::new(shaped.build(ctx))
} }
ParItem::Frame(ref frame, align) => { ParItem::Frame(ref frame, align, _) => {
ruler = ruler.max(align); ruler = ruler.max(align);
frame.clone() frame.clone()
} }
@ -528,7 +559,11 @@ impl<'a> LineLayout<'a> {
); );
offset += frame.size.w; offset += frame.size.w;
output.push_frame(pos, frame);
match item.index() {
Some(idx) => output.push_indexed_frame(pos, idx, frame),
None => output.push_frame(pos, frame),
}
}); });
output output

View File

@ -35,6 +35,7 @@ pub fn new() -> Scope {
std.def_func("strike", strike); std.def_func("strike", strike);
std.def_func("underline", underline); std.def_func("underline", underline);
std.def_func("overline", overline); std.def_func("overline", overline);
std.def_func("link", link);
// Layout. // Layout.
std.def_func("page", page); std.def_func("page", page);

View File

@ -1,4 +1,4 @@
use crate::eval::{FontState, LineState}; use crate::eval::{Decoration, FontState, LineState};
use crate::layout::Paint; use crate::layout::Paint;
use super::*; use super::*;
@ -197,3 +197,18 @@ fn line_impl(
Ok(Value::Template(template)) Ok(Value::Template(template))
} }
/// `link`: Set a link.
pub fn link(_: &mut EvalContext, args: &mut Arguments) -> TypResult<Value> {
let url = args.expect::<Str>("url")?;
let mut body = args.eat().unwrap_or_else(|| {
let mut template = Template::new();
template.text(&url);
template
});
body.decorate(Decoration::Link(url.into()));
Ok(Value::Template(body))
}

BIN
tests/ref/text/links.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

12
tests/typ/text/links.typ Normal file
View File

@ -0,0 +1,12 @@
// Link without body.
#link("https://example.com/")
// Link with body.
#link("https://typst.app/")[Some text text text]
// With line break.
This link appears #link("https://google.com/")[in the middle of] a paragraph.
// Styled with underline and color.
#let link(url, body) = link(url, [#font(fill: rgb("283663")) #underline(body)])
You could also make the #link("https://html5zombo.com/")[link look way more typical.]

View File

@ -9,7 +9,7 @@ use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder}; use ttf_parser::{GlyphId, OutlineBuilder};
use walkdir::WalkDir; use walkdir::WalkDir;
use typst::color::Color; use typst::color::{Color, RgbaColor};
use typst::diag::Error; use typst::diag::Error;
use typst::eval::{State, Value}; use typst::eval::{State, Value};
use typst::geom::{self, Length, PathElement, Point, Sides, Size}; use typst::geom::{self, Length, PathElement, Point, Sides, Size};
@ -428,6 +428,11 @@ fn draw(ctx: &Context, frames: &[Rc<Frame>], dpi: f32) -> sk::Pixmap {
Element::Image(id, size) => { Element::Image(id, size) => {
draw_image(&mut canvas, ts, ctx, id, size); draw_image(&mut canvas, ts, ctx, id, size);
} }
Element::Link(_, s) => {
let outline = Geometry::Rect(s);
let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40)));
draw_geometry(&mut canvas, ts, &outline, paint);
}
} }
} }