mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Basic floating placement
This commit is contained in:
parent
0e8492eac1
commit
78f96f844b
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
BIN
tests/ref/layout/place-float-auto.png
Normal file
BIN
tests/ref/layout/place-float-auto.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
tests/ref/layout/place-float-figure.png
Normal file
BIN
tests/ref/layout/place-float-figure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 129 KiB |
19
tests/typ/layout/place-float-auto.typ
Normal file
19
tests/typ/layout/place-float-auto.typ
Normal 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]
|
21
tests/typ/layout/place-float-figure.typ
Normal file
21
tests/typ/layout/place-float-figure.typ
Normal 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%),
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user