diff options
| -rw-r--r-- | src/eval/template.rs | 74 | ||||
| -rw-r--r-- | src/eval/walk.rs | 1 | ||||
| -rw-r--r-- | src/export/pdf.rs | 43 | ||||
| -rw-r--r-- | src/layout/frame.rs | 25 | ||||
| -rw-r--r-- | src/layout/par.rs | 89 | ||||
| -rw-r--r-- | src/library/mod.rs | 1 | ||||
| -rw-r--r-- | src/library/text.rs | 17 | ||||
| -rw-r--r-- | tests/ref/text/links.png | bin | 0 -> 6947 bytes | |||
| -rw-r--r-- | tests/typ/text/links.typ | 12 | ||||
| -rw-r--r-- | tests/typeset.rs | 7 |
10 files changed, 196 insertions, 73 deletions
diff --git a/src/eval/template.rs b/src/eval/template.rs index addbb466..0ab49d04 100644 --- a/src/eval/template.rs +++ b/src/eval/template.rs @@ -20,7 +20,7 @@ pub struct Template(Rc<Vec<TemplateNode>>); #[derive(Clone)] enum TemplateNode { /// A word space. - Space, + Space(Vec<Decoration>), /// A line break. Linebreak, /// A paragraph break. @@ -28,11 +28,11 @@ enum TemplateNode { /// A page break. Pagebreak(bool), /// Plain text. - Text(EcoString), + Text(EcoString, Vec<Decoration>), /// Spacing. Spacing(GenAxis, Linear), /// An inline node builder. - Inline(Rc<dyn Fn(&State) -> LayoutNode>), + Inline(Rc<dyn Fn(&State) -> LayoutNode>, Vec<Decoration>), /// An block node builder. Block(Rc<dyn Fn(&State) -> LayoutNode>), /// Save the current state. @@ -43,6 +43,13 @@ enum TemplateNode { 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 { /// Create a new, empty template. pub fn new() -> Self { @@ -55,7 +62,7 @@ impl Template { F: Fn(&State) -> T + 'static, 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])) } @@ -71,7 +78,7 @@ impl Template { /// Add a word space to the template. 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. @@ -91,7 +98,7 @@ impl Template { /// Add text to the template. 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. @@ -107,6 +114,19 @@ impl Template { 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. pub fn save(&mut self) { self.make_mut().push(TemplateNode::Save); @@ -201,7 +221,7 @@ impl Add<Str> for Template { type Output = Self; 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 } } @@ -210,7 +230,7 @@ impl Add<Template> for Str { type Output = Template; 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 } } @@ -261,26 +281,26 @@ impl Builder { self.pagebreak(true, false); } } - TemplateNode::Space => self.space(), + TemplateNode::Space(decos) => self.space(decos), TemplateNode::Linebreak => self.linebreak(), TemplateNode::Parbreak => self.parbreak(), 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::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::Modify(f) => f(&mut self.state), } } /// Push a word space into the active paragraph. - fn space(&mut self) { - self.stack.par.push_soft(self.make_text_node(' ')); + fn space(&mut self, decos: &[Decoration]) { + self.stack.par.push_soft(self.make_text_node(' ', decos.to_vec())); } /// Apply a forced line break. 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. @@ -300,16 +320,14 @@ impl Builder { } /// Push text into the active paragraph. - /// - /// The text is split into lines at newlines. - fn text(&mut self, text: impl Into<EcoString>) { - self.stack.par.push(self.make_text_node(text)); + fn text(&mut self, text: impl Into<EcoString>, decos: &[Decoration]) { + self.stack.par.push(self.make_text_node(text, decos.to_vec())); } /// 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; - 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. @@ -348,11 +366,16 @@ impl Builder { /// Construct a text node with the given text and settings from the active /// 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( text.into(), self.state.aligns.inline, Rc::clone(&self.state.font), + decos, ) } } @@ -465,11 +488,14 @@ impl ParBuilder { } fn push_inner(&mut self, child: ParChild) { - if let ParChild::Text(curr_text, curr_align, curr_props) = &child { - if let Some(ParChild::Text(prev_text, prev_align, prev_props)) = + if let ParChild::Text(curr_text, curr_align, curr_props, curr_decos) = &child { + if let Some(ParChild::Text(prev_text, prev_align, prev_props, prev_decos)) = 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); return; } diff --git a/src/eval/walk.rs b/src/eval/walk.rs index d99db3ed..db9fbbde 100644 --- a/src/eval/walk.rs +++ b/src/eval/walk.rs @@ -115,6 +115,7 @@ fn walk_item(ctx: &mut EvalContext, label: EcoString, body: Template) { label.clone(), state.aligns.inline, Rc::clone(&state.font), + vec![], )], }; StackNode { diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 7ff600ff..d613efc3 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -8,8 +8,8 @@ use std::rc::Rc; use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba}; use miniz_oxide::deflate; use pdf_writer::{ - CidFontType, ColorSpace, Content, Filter, FontFlags, Name, PdfWriter, Rect, Ref, Str, - SystemInfo, UnicodeCmap, + ActionType, AnnotationType, CidFontType, ColorSpace, Content, Filter, FontFlags, + Name, PdfWriter, Rect, Ref, Str, SystemInfo, UnicodeCmap, }; use ttf_parser::{name_id, GlyphId}; @@ -59,6 +59,7 @@ impl<'a> PdfExporter<'a> { } image_map.insert(id); } + Element::Link(_, _) => {} } } } @@ -116,16 +117,34 @@ impl<'a> PdfExporter<'a> { for ((page_id, content_id), page) in self.refs.pages().zip(self.refs.contents()).zip(self.frames) { - self.writer - .page(page_id) + let w = page.size.w.to_pt() as f32; + 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) - .media_box(Rect::new( - 0.0, - 0.0, - page.size.w.to_pt() as f32, - page.size.h.to_pt() as f32, - )) - .contents(content_id); + .media_box(Rect::new(0.0, 0.0, w, h)); + + let mut annotations = page_writer.annotations(); + for (pos, element) in page.elements() { + if let Element::Link(href, size) = element { + let x = pos.x.to_pt() as f32; + 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.restore_state(); } + + Element::Link(_, _) => {} } } diff --git a/src/layout/frame.rs b/src/layout/frame.rs index e52e2751..15ef541b 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -16,17 +16,17 @@ pub struct Frame { /// The baseline of the frame measured from the top. pub baseline: Length, /// 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 /// nested frame. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -enum Child { +pub enum FrameChild { /// A leaf node in the frame tree. Element(Element), - /// An interior node. - Frame(Rc<Frame>), + /// An interior node with an optional index. + Frame(Option<usize>, Rc<Frame>), } impl Frame { @@ -38,17 +38,22 @@ impl Frame { /// Add an element at a position in the foreground. 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. 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. 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 @@ -85,12 +90,12 @@ impl<'a> Iterator for Elements<'a> { fn next(&mut self) -> Option<Self::Item> { let (cursor, offset, frame) = self.stack.last_mut()?; match frame.children.get(*cursor) { - Some((pos, Child::Frame(f))) => { + Some((pos, FrameChild::Frame(_, f))) => { let new_offset = *offset + *pos; self.stack.push((0, new_offset, f.as_ref())); self.next() } - Some((pos, Child::Element(e))) => { + Some((pos, FrameChild::Element(e))) => { *cursor += 1; Some((*offset + *pos, e)) } @@ -115,6 +120,8 @@ pub enum Element { Geometry(Geometry, Paint), /// A raster image. Image(ImageId, Size), + /// A link to an external resource. + Link(String, Size), } /// A run of shaped text. diff --git a/src/layout/par.rs b/src/layout/par.rs index 3df742a7..e92e5a18 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -4,7 +4,7 @@ use unicode_bidi::{BidiInfo, Level}; use xi_unicode::LineBreakIterator; use super::*; -use crate::eval::FontState; +use crate::eval::{Decoration, FontState}; use crate::util::{EcoString, RangeExt, SliceExt}; type Range = std::ops::Range<usize>; @@ -26,9 +26,9 @@ pub enum ParChild { /// Spacing between other nodes. Spacing(Linear), /// 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(LayoutNode, Align), + Any(LayoutNode, Align, Vec<Decoration>), } impl Layout for ParNode { @@ -48,7 +48,7 @@ impl Layout for ParNode { let layouter = ParLayouter::new(self, ctx, regions, bidi); // 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> { self.children.iter().map(|child| match child { ParChild::Spacing(_) => " ", - ParChild::Text(ref piece, _, _) => piece, - ParChild::Any(_, _) => "\u{FFFC}", + ParChild::Text(ref piece, ..) => piece, + ParChild::Any(..) => "\u{FFFC}", }) } } @@ -119,25 +119,25 @@ impl<'a> ParLayouter<'a> { let mut ranges = vec![]; // 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 { ParChild::Spacing(amount) => { let resolved = amount.resolve(regions.current.w); items.push(ParItem::Spacing(resolved)); ranges.push(range); } - ParChild::Text(_, align, ref state) => { + ParChild::Text(_, align, ref state, _) => { // TODO: Also split by language and script. for (subrange, dir) in split_runs(&bidi, range) { let text = &bidi.text[subrange.clone()]; let shaped = shape(ctx, text, dir, state); - items.push(ParItem::Text(shaped, align)); + items.push(ParItem::Text(shaped, align, i)); ranges.push(subrange); } } - ParChild::Any(ref node, align) => { + ParChild::Any(ref node, align, _) => { 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); } } @@ -156,9 +156,10 @@ impl<'a> ParLayouter<'a> { fn layout( self, ctx: &mut LayoutContext, + children: &[ParChild], regions: Regions, ) -> 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. // Invariant: Always fits into `stack.regions.current`. @@ -273,9 +274,9 @@ enum ParItem<'a> { /// Spacing between other items. Spacing(Length), /// A shaped text run with consistent direction. - Text(ShapedText<'a>, Align), + Text(ShapedText<'a>, Align, usize), /// A layouted child node. - Frame(Rc<Frame>, Align), + Frame(Rc<Frame>, Align, usize), } impl ParItem<'_> { @@ -283,8 +284,8 @@ impl ParItem<'_> { pub fn size(&self) -> Size { match self { Self::Spacing(amount) => Size::new(*amount, Length::zero()), - Self::Text(shaped, _) => shaped.size, - Self::Frame(frame, _) => frame.size, + Self::Text(shaped, ..) => shaped.size, + Self::Frame(frame, ..) => frame.size, } } @@ -292,8 +293,17 @@ impl ParItem<'_> { pub fn baseline(&self) -> Length { match self { Self::Spacing(_) => Length::zero(), - Self::Text(shaped, _) => shaped.baseline, - Self::Frame(frame, _) => frame.baseline, + Self::Text(shaped, ..) => shaped.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. struct LineStack<'a> { line_spacing: Length, + children: &'a [ParChild], full: Size, regions: Regions, size: Size, @@ -312,11 +323,12 @@ struct LineStack<'a> { impl<'a> LineStack<'a> { /// 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 { line_spacing, - constraints: Constraints::new(regions.expand), + children, full: regions.current, + constraints: Constraints::new(regions.expand), regions, size: Size::zero(), lines: vec![], @@ -368,6 +380,25 @@ impl<'a> LineStack<'a> { 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.regions.next(); self.full = self.regions.current; @@ -426,7 +457,7 @@ impl<'a> LineLayout<'a> { // Reshape the last item if it's split in half. 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 // end of the line. let base = par.ranges[last_idx].start; @@ -442,7 +473,7 @@ impl<'a> LineLayout<'a> { if !range.is_empty() || rest.is_empty() { // Reshape that part. let reshaped = shaped.reshape(ctx, range); - last = Some(ParItem::Text(reshaped, *align)); + last = Some(ParItem::Text(reshaped, *align, *i)); } items = rest; @@ -452,7 +483,7 @@ impl<'a> LineLayout<'a> { // Reshape the start item if it's split in half. 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. let Range { start: base, end: first_end } = par.ranges[first_idx]; let start = line.start; @@ -463,7 +494,7 @@ impl<'a> LineLayout<'a> { if range.len() < shaped.text.len() { if !range.is_empty() { let reshaped = shaped.reshape(ctx, range); - first = Some(ParItem::Text(reshaped, *align)); + first = Some(ParItem::Text(reshaped, *align, *i)); } items = rest; @@ -511,11 +542,11 @@ impl<'a> LineLayout<'a> { offset += amount; return; } - ParItem::Text(ref shaped, align) => { + ParItem::Text(ref shaped, align, _) => { ruler = ruler.max(align); Rc::new(shaped.build(ctx)) } - ParItem::Frame(ref frame, align) => { + ParItem::Frame(ref frame, align, _) => { ruler = ruler.max(align); frame.clone() } @@ -528,7 +559,11 @@ impl<'a> LineLayout<'a> { ); 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 diff --git a/src/library/mod.rs b/src/library/mod.rs index 44f1f01f..9d25a008 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -35,6 +35,7 @@ pub fn new() -> Scope { std.def_func("strike", strike); std.def_func("underline", underline); std.def_func("overline", overline); + std.def_func("link", link); // Layout. std.def_func("page", page); diff --git a/src/library/text.rs b/src/library/text.rs index bde2a9aa..cfd2de99 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -1,4 +1,4 @@ -use crate::eval::{FontState, LineState}; +use crate::eval::{Decoration, FontState, LineState}; use crate::layout::Paint; use super::*; @@ -197,3 +197,18 @@ fn line_impl( 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)) +} diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png Binary files differnew file mode 100644 index 00000000..5ac7ee00 --- /dev/null +++ b/tests/ref/text/links.png diff --git a/tests/typ/text/links.typ b/tests/typ/text/links.typ new file mode 100644 index 00000000..eabb316a --- /dev/null +++ b/tests/typ/text/links.typ @@ -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.] diff --git a/tests/typeset.rs b/tests/typeset.rs index 80eb9da4..b788f74d 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -9,7 +9,7 @@ use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; use walkdir::WalkDir; -use typst::color::Color; +use typst::color::{Color, RgbaColor}; use typst::diag::Error; use typst::eval::{State, Value}; 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) => { 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); + } } } |
