summaryrefslogtreecommitdiff
path: root/src/library/elements.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/library/elements.rs')
-rw-r--r--src/library/elements.rs135
1 files changed, 135 insertions, 0 deletions
diff --git a/src/library/elements.rs b/src/library/elements.rs
new file mode 100644
index 00000000..b1b5c1f9
--- /dev/null
+++ b/src/library/elements.rs
@@ -0,0 +1,135 @@
+use std::f64::consts::SQRT_2;
+
+use decorum::N64;
+
+use super::*;
+use crate::color::Color;
+use crate::layout::{
+ BackgroundNode, BackgroundShape, Fill, FixedNode, ImageNode, PadNode,
+};
+
+/// `image`: An image.
+pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
+ let path = args.expect::<Spanned<String>>(ctx, "path to image file");
+ let width = args.named(ctx, "width");
+ let height = args.named(ctx, "height");
+
+ let mut node = None;
+ if let Some(path) = &path {
+ if let Some((resolved, _)) = ctx.resolve(&path.v, path.span) {
+ if let Some(id) = ctx.cache.image.load(ctx.loader, &resolved) {
+ node = Some(ImageNode { id, width, height });
+ } else {
+ ctx.diag(error!(path.span, "failed to load image"));
+ }
+ }
+ }
+
+ Value::template("image", move |ctx| {
+ if let Some(node) = node {
+ ctx.push_into_par(node);
+ }
+ })
+}
+
+/// `rect`: A rectangle with optional content.
+pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
+ let width = args.named(ctx, "width");
+ let height = args.named(ctx, "height");
+ let fill = args.named(ctx, "fill");
+ let body = args.eat::<TemplateValue>(ctx).unwrap_or_default();
+ rect_impl("rect", width, height, None, fill, body)
+}
+
+/// `square`: A square with optional content.
+pub fn square(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
+ let length = args.named::<Length>(ctx, "length").map(Linear::from);
+ let width = length.or_else(|| args.named(ctx, "width"));
+ let height = width.is_none().then(|| args.named(ctx, "height")).flatten();
+ let fill = args.named(ctx, "fill");
+ let body = args.eat::<TemplateValue>(ctx).unwrap_or_default();
+ rect_impl("square", width, height, Some(N64::from(1.0)), fill, body)
+}
+
+fn rect_impl(
+ name: &str,
+ width: Option<Linear>,
+ height: Option<Linear>,
+ aspect: Option<N64>,
+ fill: Option<Color>,
+ body: TemplateValue,
+) -> Value {
+ Value::template(name, move |ctx| {
+ let mut stack = ctx.exec_template_stack(&body);
+ stack.aspect = aspect;
+
+ let fixed = FixedNode { width, height, child: stack.into() };
+
+ if let Some(color) = fill {
+ ctx.push_into_par(BackgroundNode {
+ shape: BackgroundShape::Rect,
+ fill: Fill::Color(color),
+ child: fixed.into(),
+ });
+ } else {
+ ctx.push_into_par(fixed);
+ }
+ })
+}
+
+/// `ellipse`: An ellipse with optional content.
+pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
+ let width = args.named(ctx, "width");
+ let height = args.named(ctx, "height");
+ let fill = args.named(ctx, "fill");
+ let body = args.eat::<TemplateValue>(ctx).unwrap_or_default();
+ ellipse_impl("ellipse", width, height, None, fill, body)
+}
+
+/// `circle`: A circle with optional content.
+pub fn circle(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
+ let radius = args.named::<Length>(ctx, "radius").map(|r| 2.0 * Linear::from(r));
+ let width = radius.or_else(|| args.named(ctx, "width"));
+ let height = width.is_none().then(|| args.named(ctx, "height")).flatten();
+ let fill = args.named(ctx, "fill");
+ let body = args.eat::<TemplateValue>(ctx).unwrap_or_default();
+ ellipse_impl("circle", width, height, Some(N64::from(1.0)), fill, body)
+}
+
+fn ellipse_impl(
+ name: &str,
+ width: Option<Linear>,
+ height: Option<Linear>,
+ aspect: Option<N64>,
+ fill: Option<Color>,
+ body: TemplateValue,
+) -> Value {
+ Value::template(name, move |ctx| {
+ // This padding ratio ensures that the rectangular padded region fits
+ // perfectly into the ellipse.
+ const PAD: f64 = 0.5 - SQRT_2 / 4.0;
+
+ let mut stack = ctx.exec_template_stack(&body);
+ stack.aspect = aspect;
+
+ let fixed = FixedNode {
+ width,
+ height,
+ child: PadNode {
+ padding: Sides::splat(Relative::new(PAD).into()),
+ child: stack.into(),
+ }
+ .into(),
+ };
+
+ if let Some(color) = fill {
+ ctx.push_into_par(BackgroundNode {
+ shape: BackgroundShape::Ellipse,
+ fill: Fill::Color(color),
+ child: fixed.into(),
+ });
+ } else {
+ ctx.push_into_par(fixed);
+ }
+ })
+}