mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Basic tables
This commit is contained in:
parent
4abdafcd15
commit
0c5243fa80
@ -17,10 +17,10 @@ use std::rc::Rc;
|
||||
|
||||
use crate::eval::{StyleChain, Styled};
|
||||
use crate::font::FontStore;
|
||||
use crate::frame::Frame;
|
||||
use crate::geom::{Align, Linear, Point, Sides, Size, Spec};
|
||||
use crate::frame::{Element, Frame, Geometry, Shape, Stroke};
|
||||
use crate::geom::{Align, Linear, Paint, Point, Sides, Size, Spec};
|
||||
use crate::image::ImageStore;
|
||||
use crate::library::{AlignNode, Move, PadNode, PageNode, SizedNode, TransformNode};
|
||||
use crate::library::{AlignNode, Move, PadNode, PageNode, TransformNode};
|
||||
use crate::Context;
|
||||
|
||||
/// The root layout node, a document consisting of top-level page runs.
|
||||
@ -153,6 +153,16 @@ impl PackedNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill the frames resulting from a node.
|
||||
pub fn filled(self, fill: Paint) -> Self {
|
||||
FillNode { fill, child: self }.pack()
|
||||
}
|
||||
|
||||
/// Stroke the frames resulting from a node.
|
||||
pub fn stroked(self, stroke: Stroke) -> Self {
|
||||
StrokeNode { stroke, child: self }.pack()
|
||||
}
|
||||
|
||||
/// Set alignments for this node.
|
||||
pub fn aligned(self, aligns: Spec<Option<Align>>) -> Self {
|
||||
if aligns.any(Option::is_some) {
|
||||
@ -294,3 +304,107 @@ where
|
||||
state.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A node that sizes its child.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct SizedNode {
|
||||
/// How to size the node horizontally and vertically.
|
||||
pub sizing: Spec<Option<Linear>>,
|
||||
/// The node to be sized.
|
||||
pub child: PackedNode,
|
||||
}
|
||||
|
||||
impl Layout for SizedNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
styles: StyleChain,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let is_auto = self.sizing.map_is_none();
|
||||
let is_rel = self.sizing.map(|s| s.map_or(false, Linear::is_relative));
|
||||
|
||||
// The "pod" is the region into which the child will be layouted.
|
||||
let pod = {
|
||||
// Resolve the sizing to a concrete size.
|
||||
let size = self
|
||||
.sizing
|
||||
.zip(regions.base)
|
||||
.map(|(s, b)| s.map(|v| v.resolve(b)))
|
||||
.unwrap_or(regions.current);
|
||||
|
||||
// Select the appropriate base and expansion for the child depending
|
||||
// on whether it is automatically or linearly sized.
|
||||
let base = is_auto.select(regions.base, size);
|
||||
let expand = regions.expand | !is_auto;
|
||||
|
||||
Regions::one(size, base, expand)
|
||||
};
|
||||
|
||||
let mut frames = self.child.layout(ctx, &pod, styles);
|
||||
let Constrained { item: frame, cts } = &mut frames[0];
|
||||
|
||||
// Ensure frame size matches regions size if expansion is on.
|
||||
let target = regions.expand.select(regions.current, frame.size);
|
||||
Rc::make_mut(frame).resize(target, Align::LEFT_TOP);
|
||||
|
||||
// Set base & exact constraints if the child is automatically sized
|
||||
// since we don't know what the child might have done. Also set base if
|
||||
// our sizing is relative.
|
||||
*cts = Constraints::new(regions.expand);
|
||||
cts.exact = regions.current.filter(regions.expand | is_auto);
|
||||
cts.base = regions.base.filter(is_rel | is_auto);
|
||||
|
||||
frames
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill the frames resulting from a node.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct FillNode {
|
||||
/// How to fill the frames resulting from the `child`.
|
||||
pub fill: Paint,
|
||||
/// The node to fill.
|
||||
pub child: PackedNode,
|
||||
}
|
||||
|
||||
impl Layout for FillNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
styles: StyleChain,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let mut frames = self.child.layout(ctx, regions, styles);
|
||||
for Constrained { item: frame, .. } in &mut frames {
|
||||
let shape = Shape::filled(Geometry::Rect(frame.size), self.fill);
|
||||
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
|
||||
}
|
||||
frames
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke the frames resulting from a node.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct StrokeNode {
|
||||
/// How to stroke the frames resulting from the `child`.
|
||||
pub stroke: Stroke,
|
||||
/// The node to stroke.
|
||||
pub child: PackedNode,
|
||||
}
|
||||
|
||||
impl Layout for StrokeNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
styles: StyleChain,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let mut frames = self.child.layout(ctx, regions, styles);
|
||||
for Constrained { item: frame, .. } in &mut frames {
|
||||
let shape = Shape::stroked(Geometry::Rect(frame.size), self.stroke);
|
||||
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
|
||||
}
|
||||
frames
|
||||
}
|
||||
}
|
||||
|
26
src/library/container.rs
Normal file
26
src/library/container.rs
Normal file
@ -0,0 +1,26 @@
|
||||
//! Inline- and block-level containers.
|
||||
|
||||
use super::prelude::*;
|
||||
|
||||
/// Size content and place it into a paragraph.
|
||||
pub struct BoxNode;
|
||||
|
||||
#[class]
|
||||
impl BoxNode {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
let width = args.named("width")?;
|
||||
let height = args.named("height")?;
|
||||
let body: PackedNode = args.find().unwrap_or_default();
|
||||
Ok(Node::inline(body.sized(Spec::new(width, height))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Place content into a separate flow.
|
||||
pub struct BlockNode;
|
||||
|
||||
#[class]
|
||||
impl BlockNode {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
Ok(Node::Block(args.find().unwrap_or_default()))
|
||||
}
|
||||
}
|
@ -138,6 +138,12 @@ impl<'a> FlowLayouter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if self.expand.y {
|
||||
while self.regions.backlog.len() > 0 {
|
||||
self.finish_region();
|
||||
}
|
||||
}
|
||||
|
||||
self.finish_region();
|
||||
self.finished
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ castable! {
|
||||
Value::Relative(v) => vec![TrackSizing::Linear(v.into())],
|
||||
Value::Linear(v) => vec![TrackSizing::Linear(v)],
|
||||
Value::Fractional(v) => vec![TrackSizing::Fractional(v)],
|
||||
Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?],
|
||||
Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast::<NonZeroUsize>()?.get()],
|
||||
Value::Array(values) => values
|
||||
.into_iter()
|
||||
.filter_map(|v| v.cast().ok())
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
pub mod align;
|
||||
pub mod columns;
|
||||
pub mod container;
|
||||
pub mod deco;
|
||||
pub mod flow;
|
||||
pub mod grid;
|
||||
@ -17,9 +18,9 @@ pub mod page;
|
||||
pub mod par;
|
||||
pub mod placed;
|
||||
pub mod shape;
|
||||
pub mod sized;
|
||||
pub mod spacing;
|
||||
pub mod stack;
|
||||
pub mod table;
|
||||
pub mod text;
|
||||
pub mod transform;
|
||||
pub mod utility;
|
||||
@ -27,6 +28,7 @@ pub mod utility;
|
||||
pub use self::image::*;
|
||||
pub use align::*;
|
||||
pub use columns::*;
|
||||
pub use container::*;
|
||||
pub use deco::*;
|
||||
pub use flow::*;
|
||||
pub use grid::*;
|
||||
@ -38,9 +40,9 @@ pub use page::*;
|
||||
pub use par::*;
|
||||
pub use placed::*;
|
||||
pub use shape::*;
|
||||
pub use sized::*;
|
||||
pub use spacing::*;
|
||||
pub use stack::*;
|
||||
pub use table::*;
|
||||
pub use text::*;
|
||||
pub use transform::*;
|
||||
pub use utility::*;
|
||||
@ -96,6 +98,7 @@ pub fn new() -> Scope {
|
||||
std.def_class::<HeadingNode>("heading");
|
||||
std.def_class::<ListNode<Unordered>>("list");
|
||||
std.def_class::<ListNode<Ordered>>("enum");
|
||||
std.def_class::<TableNode>("table");
|
||||
std.def_class::<ImageNode>("image");
|
||||
std.def_class::<ShapeNode<Rect>>("rect");
|
||||
std.def_class::<ShapeNode<Square>>("square");
|
||||
|
@ -98,27 +98,22 @@ impl PageNode {
|
||||
child = ColumnsNode { columns, child: self.0.clone() }.pack();
|
||||
}
|
||||
|
||||
// Realize margins with padding node.
|
||||
// Realize margins.
|
||||
child = child.padded(padding);
|
||||
|
||||
// Realize background fill.
|
||||
if let Some(fill) = styles.get(Self::FILL) {
|
||||
child = child.filled(fill);
|
||||
}
|
||||
|
||||
// Layout the child.
|
||||
let expand = size.map(Length::is_finite);
|
||||
let regions = Regions::repeat(size, size, expand);
|
||||
let mut frames: Vec<_> = child
|
||||
child
|
||||
.layout(ctx, ®ions, styles)
|
||||
.into_iter()
|
||||
.map(|c| c.item)
|
||||
.collect();
|
||||
|
||||
// Add background fill if requested.
|
||||
if let Some(fill) = styles.get(Self::FILL) {
|
||||
for frame in &mut frames {
|
||||
let shape = Shape::filled(Geometry::Rect(frame.size), fill);
|
||||
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
|
||||
}
|
||||
}
|
||||
|
||||
frames
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,80 +0,0 @@
|
||||
//! Horizontal and vertical sizing of nodes.
|
||||
|
||||
use super::prelude::*;
|
||||
|
||||
/// Size content and place it into a paragraph.
|
||||
pub struct BoxNode;
|
||||
|
||||
#[class]
|
||||
impl BoxNode {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
let width = args.named("width")?;
|
||||
let height = args.named("height")?;
|
||||
let body: PackedNode = args.find().unwrap_or_default();
|
||||
Ok(Node::inline(body.sized(Spec::new(width, height))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Place content into a separate flow.
|
||||
pub struct BlockNode;
|
||||
|
||||
#[class]
|
||||
impl BlockNode {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
Ok(Node::Block(args.find().unwrap_or_default()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A node that sizes its child.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct SizedNode {
|
||||
/// How to size the node horizontally and vertically.
|
||||
pub sizing: Spec<Option<Linear>>,
|
||||
/// The node to be sized.
|
||||
pub child: PackedNode,
|
||||
}
|
||||
|
||||
impl Layout for SizedNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
styles: StyleChain,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let is_auto = self.sizing.map_is_none();
|
||||
let is_rel = self.sizing.map(|s| s.map_or(false, Linear::is_relative));
|
||||
|
||||
// The "pod" is the region into which the child will be layouted.
|
||||
let pod = {
|
||||
// Resolve the sizing to a concrete size.
|
||||
let size = self
|
||||
.sizing
|
||||
.zip(regions.base)
|
||||
.map(|(s, b)| s.map(|v| v.resolve(b)))
|
||||
.unwrap_or(regions.current);
|
||||
|
||||
// Select the appropriate base and expansion for the child depending
|
||||
// on whether it is automatically or linearly sized.
|
||||
let base = is_auto.select(regions.base, size);
|
||||
let expand = regions.expand | !is_auto;
|
||||
|
||||
Regions::one(size, base, expand)
|
||||
};
|
||||
|
||||
let mut frames = self.child.layout(ctx, &pod, styles);
|
||||
let Constrained { item: frame, cts } = &mut frames[0];
|
||||
|
||||
// Ensure frame size matches regions size if expansion is on.
|
||||
let target = regions.expand.select(regions.current, frame.size);
|
||||
Rc::make_mut(frame).resize(target, Align::LEFT_TOP);
|
||||
|
||||
// Set base & exact constraints if the child is automatically sized
|
||||
// since we don't know what the child might have done. Also set base if
|
||||
// our sizing is relative.
|
||||
*cts = Constraints::new(regions.expand);
|
||||
cts.exact = regions.current.filter(regions.expand | is_auto);
|
||||
cts.base = regions.base.filter(is_rel | is_auto);
|
||||
|
||||
frames
|
||||
}
|
||||
}
|
101
src/library/table.rs
Normal file
101
src/library/table.rs
Normal file
@ -0,0 +1,101 @@
|
||||
//! Tabular container.
|
||||
|
||||
use super::prelude::*;
|
||||
use super::{GridNode, TrackSizing};
|
||||
|
||||
/// A table of items.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct TableNode {
|
||||
/// Defines sizing for content rows and columns.
|
||||
pub tracks: Spec<Vec<TrackSizing>>,
|
||||
/// Defines sizing of gutter rows and columns between content.
|
||||
pub gutter: Spec<Vec<TrackSizing>>,
|
||||
/// The nodes to be arranged in the table.
|
||||
pub children: Vec<PackedNode>,
|
||||
}
|
||||
|
||||
#[class]
|
||||
impl TableNode {
|
||||
/// The primary cell fill color.
|
||||
pub const PRIMARY: Option<Paint> = None;
|
||||
/// The secondary cell fill color.
|
||||
pub const SECONDARY: Option<Paint> = None;
|
||||
/// How the stroke the cells.
|
||||
pub const STROKE: Option<Paint> = Some(RgbaColor::BLACK.into());
|
||||
/// The stroke's thickness.
|
||||
pub const THICKNESS: Length = Length::pt(1.0);
|
||||
/// How much to pad the cells's content.
|
||||
pub const PADDING: Linear = Length::pt(5.0).into();
|
||||
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
let columns = args.named("columns")?.unwrap_or_default();
|
||||
let rows = args.named("rows")?.unwrap_or_default();
|
||||
let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default();
|
||||
let column_gutter = args.named("column-gutter")?;
|
||||
let row_gutter = args.named("row-gutter")?;
|
||||
Ok(Node::block(Self {
|
||||
tracks: Spec::new(columns, rows),
|
||||
gutter: Spec::new(
|
||||
column_gutter.unwrap_or_else(|| base_gutter.clone()),
|
||||
row_gutter.unwrap_or(base_gutter),
|
||||
),
|
||||
children: args.all().collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
|
||||
let fill = args.named("fill")?;
|
||||
styles.set_opt(Self::PRIMARY, args.named("primary")?.or(fill));
|
||||
styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill));
|
||||
styles.set_opt(Self::STROKE, args.named("stroke")?);
|
||||
styles.set_opt(Self::THICKNESS, args.named("thickness")?);
|
||||
styles.set_opt(Self::PADDING, args.named("padding")?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for TableNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
styles: StyleChain,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let primary = styles.get(Self::PRIMARY);
|
||||
let secondary = styles.get(Self::SECONDARY);
|
||||
let thickness = styles.get(Self::THICKNESS);
|
||||
let stroke = styles.get(Self::STROKE).map(|paint| Stroke { paint, thickness });
|
||||
let padding = styles.get(Self::PADDING);
|
||||
|
||||
let cols = self.tracks.x.len();
|
||||
let children = self
|
||||
.children
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(i, mut child)| {
|
||||
child = child.padded(Sides::splat(padding));
|
||||
|
||||
if let Some(stroke) = stroke {
|
||||
child = child.stroked(stroke);
|
||||
}
|
||||
|
||||
let x = i % cols;
|
||||
let y = i / cols;
|
||||
if let Some(fill) = [primary, secondary][(x + y) % 2] {
|
||||
child = child.filled(fill);
|
||||
}
|
||||
|
||||
child
|
||||
})
|
||||
.collect();
|
||||
|
||||
let grid = GridNode {
|
||||
tracks: self.tracks.clone(),
|
||||
gutter: self.gutter.clone(),
|
||||
children,
|
||||
};
|
||||
|
||||
grid.layout(ctx, regions, styles)
|
||||
}
|
||||
}
|
BIN
tests/ref/layout/table.png
Normal file
BIN
tests/ref/layout/table.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
9
tests/typ/layout/table.typ
Normal file
9
tests/typ/layout/table.typ
Normal file
@ -0,0 +1,9 @@
|
||||
#set page(height: 70pt)
|
||||
#set table(primary: rgb("aaa"), secondary: none)
|
||||
|
||||
#table(
|
||||
columns: (1fr,) * 3,
|
||||
stroke: rgb("333"),
|
||||
thickness: 2pt,
|
||||
[A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user