use std::borrow::Cow;
use std::num::NonZeroUsize;
use std::str::FromStr;
use ecow::EcoString;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector,
Show, ShowSet, Smart, StyleChain, Styles, Synthesize,
};
use crate::introspection::{
Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
};
use crate::layout::{
AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
PlaceElem, PlacementScope, VAlignment, VElem,
};
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::text::{Lang, Region, TextElem};
use crate::utils::NonZeroExt;
use crate::visualize::ImageElem;
#[elem(scope, Locatable, Synthesize, Count, Show, ShowSet, Refable, Outlinable)]
pub struct FigureElem {
#[required]
pub body: Content,
pub placement: Option<Smart<VAlignment>>,
pub scope: PlacementScope,
pub caption: Option<Packed<FigureCaption>>,
pub kind: Smart<FigureKind>,
#[borrowed]
pub supplement: Smart<Option<Supplement>>,
#[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
#[borrowed]
pub numbering: Option<Numbering>,
#[default(Em::new(0.65).into())]
pub gap: Length,
#[default(true)]
pub outlined: bool,
#[synthesized]
pub counter: Option<Counter>,
}
#[scope]
impl FigureElem {
#[elem]
type FigureCaption;
}
impl Synthesize for Packed<FigureElem> {
fn synthesize(
&mut self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<()> {
let span = self.span();
let location = self.location();
let elem = self.as_mut();
let numbering = elem.numbering(styles);
let kind = elem.kind(styles).unwrap_or_else(|| {
elem.body()
.query_first(Selector::can::<dyn Figurable>())
.map(|elem| FigureKind::Elem(elem.func()))
.unwrap_or_else(|| FigureKind::Elem(ImageElem::elem()))
});
let supplement = match elem.supplement(styles).as_ref() {
Smart::Auto => {
let name = match &kind {
FigureKind::Elem(func) => func
.local_name(
TextElem::lang_in(styles),
TextElem::region_in(styles),
)
.map(TextElem::packed),
FigureKind::Name(_) => None,
};
if numbering.is_some() && name.is_none() {
bail!(span, "please specify the figure's supplement")
}
Some(name.unwrap_or_default())
}
Smart::Custom(None) => None,
Smart::Custom(Some(supplement)) => {
let descendant = match kind {
FigureKind::Elem(func) => elem
.body()
.query_first(Selector::Elem(func, None))
.map(Cow::Owned),
FigureKind::Name(_) => None,
};
let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body()));
Some(supplement.resolve(engine, styles, [target])?)
}
};
let counter = Counter::new(CounterKey::Selector(
select_where!(FigureElem, Kind => kind.clone()),
));
let mut caption = elem.caption(styles);
if let Some(caption) = &mut caption {
caption.push_kind(kind.clone());
caption.push_supplement(supplement.clone());
caption.push_numbering(numbering.clone());
caption.push_counter(Some(counter.clone()));
caption.push_figure_location(location);
}
elem.push_kind(Smart::Custom(kind));
elem.push_supplement(Smart::Custom(supplement.map(Supplement::Content)));
elem.push_counter(Some(counter));
elem.push_caption(caption);
Ok(())
}
}
impl Show for Packed<FigureElem> {
#[typst_macros::time(name = "figure", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body().clone();
if let Some(caption) = self.caption(styles) {
let v = VElem::new(self.gap(styles).into()).with_weak(true).pack();
realized = match caption.position(styles) {
OuterVAlignment::Top => caption.pack() + v + realized,
OuterVAlignment::Bottom => realized + v + caption.pack(),
};
}
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span());
if let Some(align) = self.placement(styles) {
realized = PlaceElem::new(realized)
.with_alignment(align.map(|align| HAlignment::Center + align))
.with_scope(self.scope(styles))
.with_float(true)
.pack()
.spanned(self.span());
} else if self.scope(styles) == PlacementScope::Parent {
bail!(
self.span(),
"parent-scoped placement is only available for floating figures";
hint: "you can enable floating placement with `figure(placement: auto, ..)`"
);
}
Ok(realized)
}
}
impl ShowSet for Packed<FigureElem> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut map = Styles::new();
map.set(BlockElem::set_breakable(false));
map.set(AlignElem::set_alignment(Alignment::CENTER));
map
}
}
impl Count for Packed<FigureElem> {
fn update(&self) -> Option<CounterUpdate> {
self.numbering()
.is_some()
.then(|| CounterUpdate::Step(NonZeroUsize::ONE))
}
}
impl Refable for Packed<FigureElem> {
fn supplement(&self) -> Content {
match (**self).supplement(StyleChain::default()).as_ref() {
Smart::Custom(Some(Supplement::Content(content))) => content.clone(),
_ => Content::empty(),
}
}
fn counter(&self) -> Counter {
(**self)
.counter()
.cloned()
.flatten()
.unwrap_or_else(|| Counter::of(FigureElem::elem()))
}
fn numbering(&self) -> Option<&Numbering> {
(**self).numbering(StyleChain::default()).as_ref()
}
}
impl Outlinable for Packed<FigureElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.outlined(StyleChain::default()) {
return Ok(None);
}
let Some(caption) = self.caption(StyleChain::default()) else {
return Ok(None);
};
let mut realized = caption.body().clone();
if let (
Smart::Custom(Some(Supplement::Content(mut supplement))),
Some(Some(counter)),
Some(numbering),
) = (
(**self).supplement(StyleChain::default()).clone(),
(**self).counter(),
self.numbering(),
) {
let numbers = counter.display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}');
}
let separator = caption.get_separator(StyleChain::default());
realized = supplement + numbers + separator + caption.body();
}
Ok(Some(realized))
}
}
#[elem(name = "caption", Synthesize, Show)]
pub struct FigureCaption {
#[default(OuterVAlignment::Bottom)]
pub position: OuterVAlignment,
pub separator: Smart<Content>,
#[required]
pub body: Content,
#[synthesized]
pub kind: FigureKind,
#[synthesized]
pub supplement: Option<Content>,
#[synthesized]
pub numbering: Option<Numbering>,
#[synthesized]
pub counter: Option<Counter>,
#[internal]
#[synthesized]
pub figure_location: Option<Location>,
}
impl FigureCaption {
fn local_separator(lang: Lang, _: Option<Region>) -> &'static str {
match lang {
Lang::CHINESE => "\u{2003}",
Lang::FRENCH => ".\u{a0}– ",
Lang::RUSSIAN => ". ",
Lang::ENGLISH | _ => ": ",
}
}
fn get_separator(&self, styles: StyleChain) -> Content {
self.separator(styles).unwrap_or_else(|| {
TextElem::packed(Self::local_separator(
TextElem::lang_in(styles),
TextElem::region_in(styles),
))
})
}
}
impl Synthesize for Packed<FigureCaption> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
let elem = self.as_mut();
elem.push_separator(Smart::Custom(elem.get_separator(styles)));
Ok(())
}
}
impl Show for Packed<FigureCaption> {
#[typst_macros::time(name = "figure.caption", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body().clone();
if let (
Some(Some(mut supplement)),
Some(Some(numbering)),
Some(Some(counter)),
Some(Some(location)),
) = (
self.supplement().cloned(),
self.numbering(),
self.counter(),
self.figure_location(),
) {
let numbers = counter.display_at_loc(engine, *location, styles, numbering)?;
if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}');
}
realized = supplement + numbers + self.get_separator(styles) + realized;
}
Ok(realized)
}
}
cast! {
FigureCaption,
v: Content => v.unpack::<Self>().unwrap_or_else(Self::new),
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum FigureKind {
Elem(Element),
Name(EcoString),
}
cast! {
FigureKind,
self => match self {
Self::Elem(v) => v.into_value(),
Self::Name(v) => v.into_value(),
},
v: Element => Self::Elem(v),
v: EcoString => Self::Name(v),
}
pub trait Figurable {}