Basic floating placement

This commit is contained in:
Laurenz 2023-07-10 18:16:59 +02:00
parent 0e8492eac1
commit 78f96f844b
9 changed files with 333 additions and 99 deletions

View File

@ -71,14 +71,14 @@ impl Layout for FlowElem {
} else if child.is::<ColbreakElem>() { } else if child.is::<ColbreakElem>() {
if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
{ {
layouter.finish_region()?; layouter.finish_region(vt)?;
} }
} else { } else {
bail!(child.span(), "unexpected flow child"); bail!(child.span(), "unexpected flow child");
} }
} }
layouter.finish() layouter.finish(vt)
} }
} }
@ -99,6 +99,8 @@ struct FlowLayouter<'a> {
last_was_par: bool, last_was_par: bool,
/// Spacing and layouted blocks for the current region. /// Spacing and layouted blocks for the current region.
items: Vec<FlowItem>, items: Vec<FlowItem>,
/// A queue of floats.
pending_floats: Vec<FlowItem>,
/// Whether we have any footnotes in the current region. /// Whether we have any footnotes in the current region.
has_footnotes: bool, has_footnotes: bool,
/// Footnote configuration. /// Footnote configuration.
@ -126,7 +128,7 @@ enum FlowItem {
/// (to keep it together with its footnotes). /// (to keep it together with its footnotes).
Frame { frame: Frame, aligns: Axes<Align>, sticky: bool, movable: bool }, Frame { frame: Frame, aligns: Axes<Align>, sticky: bool, movable: bool },
/// An absolutely placed frame. /// An absolutely placed frame.
Placed(Frame), Placed { frame: Frame, y_align: Smart<Option<Align>>, float: bool, clearance: Abs },
/// A footnote frame (can also be the separator). /// A footnote frame (can also be the separator).
Footnote(Frame), Footnote(Frame),
} }
@ -136,7 +138,7 @@ impl FlowItem {
fn height(&self) -> Abs { fn height(&self) -> Abs {
match self { match self {
Self::Absolute(v, _) => *v, Self::Absolute(v, _) => *v,
Self::Fractional(_) | Self::Placed(_) => Abs::zero(), Self::Fractional(_) | Self::Placed { .. } => Abs::zero(),
Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(), Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(),
} }
} }
@ -159,6 +161,7 @@ impl<'a> FlowLayouter<'a> {
initial: regions.size, initial: regions.size,
last_was_par: false, last_was_par: false,
items: vec![], items: vec![],
pending_floats: vec![],
has_footnotes: false, has_footnotes: false,
footnote_config: FootnoteConfig { footnote_config: FootnoteConfig {
separator: FootnoteEntry::separator_in(styles), separator: FootnoteEntry::separator_in(styles),
@ -216,7 +219,7 @@ impl<'a> FlowLayouter<'a> {
if let Some(first) = lines.first() { if let Some(first) = lines.first() {
if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() { if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() {
let carry: Vec<_> = self.items.drain(sticky..).collect(); let carry: Vec<_> = self.items.drain(sticky..).collect();
self.finish_region()?; self.finish_region(vt)?;
for item in carry { for item in carry {
self.layout_item(vt, item)?; self.layout_item(vt, item)?;
} }
@ -262,17 +265,28 @@ impl<'a> FlowLayouter<'a> {
block: &Content, block: &Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
// Placed elements that are out of flow produce placed items which // Handle placed elements.
// aren't aligned later.
if let Some(placed) = block.to::<PlaceElem>() { if let Some(placed) = block.to::<PlaceElem>() {
if placed.out_of_flow(styles) { let float = placed.float(styles);
let frame = block.layout(vt, styles, self.regions)?.into_frame(); let clearance = placed.clearance(styles);
self.layout_item(vt, FlowItem::Placed(frame))?; let y_align = placed.alignment(styles).map(|align| align.y.resolve(styles));
return Ok(()); let frame = placed.layout_inner(vt, styles, self.regions)?.into_frame();
} let item = FlowItem::Placed { frame, y_align, float, clearance };
} else if self.regions.is_full() { return self.layout_item(vt, item);
}
// Temporarily delegerate rootness to the columns.
let is_root = self.root;
if is_root && block.is::<ColumnsElem>() {
self.root = false;
self.regions.root = true;
}
let mut notes = Vec::new();
if self.regions.is_full() {
// Skip directly if region is already full. // Skip directly if region is already full.
self.finish_region()?; self.finish_region(vt)?;
} }
// How to align the block. // How to align the block.
@ -285,17 +299,9 @@ impl<'a> FlowLayouter<'a> {
} }
.resolve(styles); .resolve(styles);
// Temporarily delegerate rootness to the columns.
let is_root = self.root;
if is_root && block.is::<ColumnsElem>() {
self.root = false;
self.regions.root = true;
}
// Layout the block itself. // Layout the block itself.
let sticky = BlockElem::sticky_in(styles); let sticky = BlockElem::sticky_in(styles);
let fragment = block.layout(vt, styles, self.regions)?; let fragment = block.layout(vt, styles, self.regions)?;
let mut notes = Vec::new();
for (i, frame) in fragment.into_iter().enumerate() { for (i, frame) in fragment.into_iter().enumerate() {
// Find footnotes in the frame. // Find footnotes in the frame.
@ -304,19 +310,14 @@ impl<'a> FlowLayouter<'a> {
} }
if i > 0 { if i > 0 {
self.finish_region()?; self.finish_region(vt)?;
} }
self.layout_item( let item = FlowItem::Frame { frame, aligns, sticky, movable: false };
vt, self.layout_item(vt, item)?;
FlowItem::Frame { frame, aligns, sticky, movable: false },
)?;
} }
if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? { self.try_handle_footnotes(vt, notes)?;
self.finish_region()?;
self.handle_footnotes(vt, &mut notes, false, true)?;
}
self.root = is_root; self.root = is_root;
self.regions.root = false; self.regions.root = false;
@ -327,7 +328,7 @@ impl<'a> FlowLayouter<'a> {
/// Layout a finished frame. /// Layout a finished frame.
#[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)] #[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)]
fn layout_item(&mut self, vt: &mut Vt, item: FlowItem) -> SourceResult<()> { fn layout_item(&mut self, vt: &mut Vt, mut item: FlowItem) -> SourceResult<()> {
match item { match item {
FlowItem::Absolute(v, weak) => { FlowItem::Absolute(v, weak) => {
if weak if weak
@ -342,27 +343,68 @@ impl<'a> FlowLayouter<'a> {
} }
FlowItem::Fractional(_) => {} FlowItem::Fractional(_) => {}
FlowItem::Frame { ref frame, movable, .. } => { FlowItem::Frame { ref frame, movable, .. } => {
let size = frame.size(); let height = frame.height();
if !self.regions.size.y.fits(size.y) && !self.regions.in_last() { if !self.regions.size.y.fits(height) && !self.regions.in_last() {
self.finish_region()?; self.finish_region(vt)?;
} }
self.regions.size.y -= size.y; self.regions.size.y -= height;
if self.root && movable { if self.root && movable {
let mut notes = Vec::new(); let mut notes = Vec::new();
find_footnotes(&mut notes, frame); find_footnotes(&mut notes, frame);
self.items.push(item); self.items.push(item);
if !self.handle_footnotes(vt, &mut notes, true, false)? { if !self.handle_footnotes(vt, &mut notes, true, false)? {
let item = self.items.pop(); let item = self.items.pop();
self.finish_region()?; self.finish_region(vt)?;
self.items.extend(item); self.items.extend(item);
self.regions.size.y -= size.y; self.regions.size.y -= height;
self.handle_footnotes(vt, &mut notes, true, true)?; self.handle_footnotes(vt, &mut notes, true, true)?;
} }
return Ok(()); return Ok(());
} }
} }
FlowItem::Placed(_) => {} FlowItem::Placed { float: false, .. } => {}
FlowItem::Placed {
ref mut frame,
ref mut y_align,
float: true,
clearance,
..
} => {
// If the float doesn't fit, queue it for the next region.
if !self.regions.size.y.fits(frame.height() + clearance)
&& !self.regions.in_last()
{
self.pending_floats.push(item);
return Ok(());
}
// Select the closer placement, top or bottom.
if y_align.is_auto() {
let ratio = (self.regions.size.y
- (frame.height() + clearance) / 2.0)
/ self.regions.full;
let better_align =
if ratio <= 0.5 { Align::Bottom } else { Align::Top };
*y_align = Smart::Custom(Some(better_align));
}
// Add some clearance so that the float doesn't touch the main
// content.
frame.size_mut().y += clearance;
if *y_align == Smart::Custom(Some(Align::Bottom)) {
frame.translate(Point::with_y(clearance));
}
self.regions.size.y -= frame.height();
// Find footnotes in the frame.
if self.root {
let mut notes = vec![];
find_footnotes(&mut notes, frame);
self.try_handle_footnotes(vt, notes)?;
}
}
FlowItem::Footnote(_) => {} FlowItem::Footnote(_) => {}
} }
@ -371,7 +413,7 @@ impl<'a> FlowLayouter<'a> {
} }
/// Finish the frame for one region. /// Finish the frame for one region.
fn finish_region(&mut self) -> SourceResult<()> { fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> {
// Trim weak spacing. // Trim weak spacing.
while self while self
.items .items
@ -385,25 +427,32 @@ impl<'a> FlowLayouter<'a> {
let mut fr = Fr::zero(); let mut fr = Fr::zero();
let mut used = Size::zero(); let mut used = Size::zero();
let mut footnote_height = Abs::zero(); let mut footnote_height = Abs::zero();
let mut float_top_height = Abs::zero();
let mut float_bottom_height = Abs::zero();
let mut first_footnote = true; let mut first_footnote = true;
for item in &self.items { for item in &self.items {
match item { match item {
FlowItem::Absolute(v, _) => used.y += *v, FlowItem::Absolute(v, _) => used.y += *v,
FlowItem::Fractional(v) => fr += *v, FlowItem::Fractional(v) => fr += *v,
FlowItem::Frame { frame, .. } => { FlowItem::Frame { frame, .. } => {
let size = frame.size(); used.y += frame.height();
used.y += size.y; used.x.set_max(frame.width());
used.x.set_max(size.x);
} }
FlowItem::Placed(_) => {} FlowItem::Placed { float: false, .. } => {}
FlowItem::Placed { frame, float: true, y_align, .. } => match y_align {
Smart::Custom(Some(Align::Top)) => float_top_height += frame.height(),
Smart::Custom(Some(Align::Bottom)) => {
float_bottom_height += frame.height()
}
_ => {}
},
FlowItem::Footnote(frame) => { FlowItem::Footnote(frame) => {
let size = frame.size(); footnote_height += frame.height();
footnote_height += size.y;
if !first_footnote { if !first_footnote {
footnote_height += self.footnote_config.gap; footnote_height += self.footnote_config.gap;
} }
first_footnote = false; first_footnote = false;
used.x.set_max(size.x); used.x.set_max(frame.width());
} }
} }
} }
@ -418,9 +467,11 @@ impl<'a> FlowLayouter<'a> {
} }
let mut output = Frame::new(size); let mut output = Frame::new(size);
let mut offset = Abs::zero();
let mut ruler = Align::Top; let mut ruler = Align::Top;
let mut footnote_offset = size.y - footnote_height; let mut float_top_offset = Abs::zero();
let mut offset = float_top_height;
let mut float_bottom_offset = Abs::zero();
let mut footnote_offset = Abs::zero();
// Place all frames. // Place all frames.
for item in self.items.drain(..) { for item in self.items.drain(..) {
@ -440,13 +491,37 @@ impl<'a> FlowLayouter<'a> {
offset += frame.height(); offset += frame.height();
output.push_frame(pos, frame); output.push_frame(pos, frame);
} }
FlowItem::Footnote(frame) => { FlowItem::Placed { frame, y_align, float, .. } => {
let pos = Point::with_y(footnote_offset); let y = if float {
footnote_offset += frame.height() + self.footnote_config.gap; match y_align {
output.push_frame(pos, frame); Smart::Custom(Some(Align::Top)) => {
let y = float_top_offset;
float_top_offset += frame.height();
y
}
Smart::Custom(Some(Align::Bottom)) => {
let y = size.y - footnote_height - float_bottom_height
+ float_bottom_offset;
float_bottom_offset += frame.height();
y
}
_ => offset + ruler.position(size.y - used.y),
}
} else {
match y_align {
Smart::Custom(Some(align)) => {
align.position(size.y - frame.height())
}
_ => offset + ruler.position(size.y - used.y),
}
};
output.push_frame(Point::with_y(y), frame);
} }
FlowItem::Placed(frame) => { FlowItem::Footnote(frame) => {
output.push_frame(Point::zero(), frame); let y = size.y - footnote_height + footnote_offset;
footnote_offset += frame.height() + self.footnote_config.gap;
output.push_frame(Point::with_y(y), frame);
} }
} }
} }
@ -456,23 +531,45 @@ impl<'a> FlowLayouter<'a> {
self.regions.next(); self.regions.next();
self.initial = self.regions.size; self.initial = self.regions.size;
self.has_footnotes = false; self.has_footnotes = false;
// Try to place floats.
for item in mem::take(&mut self.pending_floats) {
self.layout_item(vt, item)?;
}
Ok(()) Ok(())
} }
/// Finish layouting and return the resulting fragment. /// Finish layouting and return the resulting fragment.
fn finish(mut self) -> SourceResult<Fragment> { fn finish(mut self, vt: &mut Vt) -> SourceResult<Fragment> {
if self.expand.y { if self.expand.y {
while !self.regions.backlog.is_empty() { while !self.regions.backlog.is_empty() {
self.finish_region()?; self.finish_region(vt)?;
} }
} }
self.finish_region()?; self.finish_region(vt)?;
while !self.items.is_empty() {
self.finish_region(vt)?;
}
Ok(Fragment::frames(self.finished)) Ok(Fragment::frames(self.finished))
} }
} }
impl FlowLayouter<'_> { impl FlowLayouter<'_> {
fn try_handle_footnotes(
&mut self,
vt: &mut Vt,
mut notes: Vec<FootnoteElem>,
) -> SourceResult<()> {
if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? {
self.finish_region(vt)?;
self.handle_footnotes(vt, &mut notes, false, true)?;
}
Ok(())
}
/// Processes all footnotes in the frame. /// Processes all footnotes in the frame.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
fn handle_footnotes( fn handle_footnotes(
@ -525,7 +622,7 @@ impl FlowLayouter<'_> {
for (i, frame) in frames.into_iter().enumerate() { for (i, frame) in frames.into_iter().enumerate() {
find_footnotes(notes, &frame); find_footnotes(notes, &frame);
if i > 0 { if i > 0 {
self.finish_region()?; self.finish_region(vt)?;
self.layout_footnote_separator(vt)?; self.layout_footnote_separator(vt)?;
self.regions.size.y -= self.footnote_config.gap; self.regions.size.y -= self.footnote_config.gap;
} }

View File

@ -27,12 +27,43 @@ use crate::prelude::*;
pub struct PlaceElem { pub struct PlaceElem {
/// Relative to which position in the parent container to place the content. /// Relative to which position in the parent container to place the content.
/// ///
/// When an axis of the page is `{auto}` sized, all alignments relative to that /// Cannot be `{auto}` if `float` is `{false}` and must be either
/// axis will be ignored, instead, the item will be placed in the origin of the /// `{auto}`, `{top}`, or `{bottom}` if `float` is `{true}`.
/// axis. ///
/// When an axis of the page is `{auto}` sized, all alignments relative to
/// that axis will be ignored, instead, the item will be placed in the
/// origin of the axis.
#[positional] #[positional]
#[default(Axes::with_x(Some(GenAlign::Start)))] #[default(Smart::Custom(Axes::with_x(Some(GenAlign::Start))))]
pub alignment: Axes<Option<GenAlign>>, pub alignment: Smart<Axes<Option<GenAlign>>>,
/// Whether the placed element has floating layout.
///
/// Floating elements are positioned at the top or bottom of the page,
/// displacing in-flow content.
///
/// ```example
/// #set page(height: 150pt)
/// #let note(where, body) = place(
/// center + where,
/// float: true,
/// clearance: 6pt,
/// rect(body),
/// )
///
/// #lorem(10)
/// #note(bottom)[Bottom 1]
/// #note(bottom)[Bottom 2]
/// #lorem(40)
/// #note(top)[Top]
/// #lorem(10)
/// ```
pub float: bool,
/// The amount of clearance the placed element has in a floating layout.
#[default(Em::new(1.5).into())]
#[resolve]
pub clearance: Length,
/// The horizontal displacement of the placed content. /// The horizontal displacement of the placed content.
/// ///
@ -61,22 +92,7 @@ impl Layout for PlaceElem {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let out_of_flow = self.out_of_flow(styles); let mut frame = self.layout_inner(vt, styles, regions)?.into_frame();
// The pod is the base area of the region because for absolute
// placement we don't really care about the already used area.
let pod = {
let finite = regions.base().map(Abs::is_finite);
let expand = finite & (regions.expand | out_of_flow);
Regions::one(regions.base(), expand)
};
let child = self
.body()
.moved(Axes::new(self.dx(styles), self.dy(styles)))
.aligned(self.alignment(styles));
let mut frame = child.layout(vt, styles, pod)?.into_frame();
// If expansion is off, zero all sizes so that we don't take up any // If expansion is off, zero all sizes so that we don't take up any
// space in our parent. Otherwise, respect the expand settings. // space in our parent. Otherwise, respect the expand settings.
@ -88,11 +104,48 @@ impl Layout for PlaceElem {
} }
impl PlaceElem { impl PlaceElem {
/// Whether this element wants to be placed relative to its its parent's /// Layout without zeroing the frame size.
/// base origin. Instead of relative to the parent's current flow/cursor pub fn layout_inner(
/// position. &self,
pub fn out_of_flow(&self, styles: StyleChain) -> bool { vt: &mut Vt,
self.alignment(styles).y.is_some() styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
// The pod is the base area of the region because for absolute
// placement we don't really care about the already used area.
let base = regions.base();
let expand =
Axes::new(base.x.is_finite(), base.y.is_finite() && !self.float(styles));
let pod = Regions::one(base, expand);
let float = self.float(styles);
let alignment = self.alignment(styles);
if float
&& !matches!(
alignment,
Smart::Auto
| Smart::Custom(Axes {
y: Some(GenAlign::Specific(Align::Top | Align::Bottom)),
..
})
)
{
bail!(self.span(), "floating placement must be `auto`, `top`, or `bottom`");
} else if !float && alignment.is_auto() {
return Err("automatic positioning is only available for floating placement")
.hint("you can enable floating placement with `place(float: true, ..)`")
.at(self.span());
}
let child = self
.body()
.moved(Axes::new(self.dx(styles), self.dy(styles)))
.aligned(
alignment.unwrap_or_else(|| Axes::with_x(Some(Align::Center.into()))),
);
child.layout(vt, styles, pod)
} }
} }

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use super::{ use super::{
Count, Counter, CounterKey, CounterUpdate, LocalName, Numbering, NumberingPattern, Count, Counter, CounterKey, CounterUpdate, LocalName, Numbering, NumberingPattern,
}; };
use crate::layout::{BlockElem, VElem}; use crate::layout::{BlockElem, PlaceElem, VElem};
use crate::meta::{Outlinable, Refable, Supplement}; use crate::meta::{Outlinable, Refable, Supplement};
use crate::prelude::*; use crate::prelude::*;
use crate::text::TextElem; use crate::text::TextElem;
@ -72,7 +72,7 @@ use crate::visualize::ImageElem;
/// ///
/// If your figure is too large and its contents are breakable across pages /// If your figure is too large and its contents are breakable across pages
/// (e.g. if it contains a large table), then you can make the figure breakable /// (e.g. if it contains a large table), then you can make the figure breakable
/// across pages as well by using `#show figure: set block(breakable: true)` /// across pages as well by using `[#show figure: set block(breakable: true)]`
/// (see the [block]($func/block) documentation for more information). /// (see the [block]($func/block) documentation for more information).
/// ///
/// Display: Figure /// Display: Figure
@ -83,29 +83,49 @@ pub struct FigureElem {
#[required] #[required]
pub body: Content, pub body: Content,
/// The figure's placement on the page.
///
/// - `{none}`: The figure stays in-flow exactly where it was specified
/// like other content.
/// - `{auto}`: The figure picks `{top}` or `{bottom}` depending on which
/// is closer.
/// - `{top}`: The figure floats to the top of the page.
/// - `{bottom}`: The figure floats to the bottom of the page.
///
/// ```example
/// #set page(height: 200pt)
///
/// = Introduction
/// #figure(
/// placement: bottom,
/// caption: [A glacier],
/// image("glacier.jpg", width: 60%),
/// )
/// #lorem(60)
/// ```
pub placement: Option<Smart<VerticalAlign>>,
/// The figure's caption. /// The figure's caption.
pub caption: Option<Content>, pub caption: Option<Content>,
/// The caption's position. /// The caption's position. Either `{top}` or `{bottom}`.
///
/// You can set the caption position to `{top}` or `{bottom}`, defaults to
/// `{bottom}`.
/// ///
/// ```example /// ```example
/// #figure( /// #figure(
/// table(columns: 2)[A][B], /// table(columns: 2)[A][B],
// caption: [I'm up here], /// caption: [I'm up here],
/// caption-pos: top,
/// ) /// )
///
/// #figure( /// #figure(
/// table(columns: 2)[A][B], /// table(columns: 2)[A][B],
/// caption: [I'm down here], /// caption: [I'm down here],
/// caption-pos: bottom,
/// ) /// )
/// ``` /// ```
#[default(VerticalAlign(GenAlign::Specific(Align::Bottom)))] #[default(VerticalAlign(GenAlign::Specific(Align::Bottom)))]
pub caption_pos: VerticalAlign, pub caption_pos: VerticalAlign,
/// The kind of the figure this is. /// The kind of figure this is.
/// ///
/// If set to `{auto}`, the figure will try to automatically determine its /// If set to `{auto}`, the figure will try to automatically determine its
/// kind. All figures of the same kind share a common counter. /// kind. All figures of the same kind share a common counter.
@ -247,6 +267,7 @@ impl Synthesize for FigureElem {
}), }),
))); )));
self.push_placement(self.placement(styles));
self.push_caption_pos(caption_pos); self.push_caption_pos(caption_pos);
self.push_caption(self.caption(styles)); self.push_caption(self.caption(styles));
self.push_kind(Smart::Custom(kind)); self.push_kind(Smart::Custom(kind));
@ -278,10 +299,22 @@ impl Show for FigureElem {
}; };
// Wrap the contents in a block. // Wrap the contents in a block.
Ok(BlockElem::new() realized = BlockElem::new()
.with_body(Some(realized)) .with_body(Some(realized))
.pack() .pack()
.aligned(Axes::with_x(Some(Align::Center.into())))) .aligned(Axes::with_x(Some(Align::Center.into())));
// Wrap in a float.
if let Some(align) = self.placement(styles) {
realized = PlaceElem::new(realized)
.with_float(true)
.with_alignment(align.map(|VerticalAlign(align)| {
Axes::new(Some(Align::Center.into()), Some(align))
}))
.pack();
}
Ok(realized)
} }
} }

View File

@ -215,9 +215,15 @@ pub trait Hint<T> {
fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>; fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>;
} }
impl<T> Hint<T> for StrResult<T> { impl<T, S> Hint<T> for Result<T, S>
where
S: Into<EcoString>,
{
fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> { fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
self.map_err(|message| HintedString { message, hints: vec![hint.into()] }) self.map_err(|message| HintedString {
message: message.into(),
hints: vec![hint.into()],
})
} }
} }

View File

@ -338,6 +338,12 @@ impl Frame {
impl Frame { impl Frame {
/// Add a full size aqua background and a red baseline for debugging. /// Add a full size aqua background and a red baseline for debugging.
pub fn debug(mut self) -> Self { pub fn debug(mut self) -> Self {
self.debug_in_place();
self
}
/// Debug in place.
pub fn debug_in_place(&mut self) {
self.insert( self.insert(
0, 0,
Point::zero(), Point::zero(),
@ -359,7 +365,6 @@ impl Frame {
Span::detached(), Span::detached(),
), ),
); );
self
} }
/// Add a green marker at a position for debugging. /// Add a green marker at a position for debugging.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -0,0 +1,19 @@
// Test floating placement.
---
#set page(height: 140pt)
#set place(clearance: 5pt)
#lorem(6)
#place(auto, float: true, rect[A])
#place(auto, float: true, rect[B])
#place(auto, float: true, rect[C])
#place(auto, float: true, rect[D])
---
// Error: 2-20 automatic positioning is only available for floating placement
// Hint: 2-20 you can enable floating placement with `place(float: true, ..)`
#place(auto)[Hello]
---
// Error: 2-45 floating placement must be `auto`, `top`, or `bottom`
#place(center + horizon, float: true)[Hello]

View File

@ -0,0 +1,21 @@
// Test floating figures.
---
#set page(height: 250pt, width: 150pt)
= Introduction
#lorem(10) #footnote[Lots of Latin]
#figure(
placement: bottom,
caption: [A glacier #footnote[Lots of Ice]],
image("/files/glacier.jpg", width: 80%),
)
#lorem(40)
#figure(
placement: top,
caption: [An important],
image("/files/diagram.svg", width: 80%),
)