Overline, Underline, Overbracket, Underbracket

This commit is contained in:
Laurenz 2023-01-28 23:13:11 +01:00
parent 406de22ee5
commit 76048a8ef4
7 changed files with 326 additions and 179 deletions

View File

@ -1,123 +0,0 @@
use super::*;
const BRACED_GAP: Em = Em::new(0.3);
/// # Underbrace
/// A horizontal brace under content, with an optional annotation below.
///
/// ## Example
/// ```
/// $ underbrace(1 + 2 + ... + 5, "numbers") $
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content above the brace.
///
/// - annotation: Content (positional)
/// The optional content below the brace.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct UnderbraceNode {
/// The content above the brace.
pub body: Content,
/// The optional content below the brace.
pub annotation: Option<Content>,
}
#[node]
impl UnderbraceNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let body = args.expect("body")?;
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
}
impl LayoutMath for UnderbraceNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let gap = BRACED_GAP.scaled(ctx);
let body = ctx.layout_row(&self.body)?;
let glyph = GlyphFragment::new(ctx, '⏟');
let brace = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
let mut rows = vec![body, brace.into()];
ctx.style(ctx.style.for_subscript());
rows.extend(
self.annotation
.as_ref()
.map(|annotation| ctx.layout_row(annotation))
.transpose()?,
);
ctx.unstyle();
ctx.push(stack(ctx, rows, Align::Center, gap, 0));
Ok(())
}
}
/// # Overbrace
/// A horizontal brace over content, with an optional annotation above.
///
/// ## Example
/// ```
/// $ overbrace(1 + 2 + ... + 5, "numbers") $
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content below the brace.
///
/// - annotation: Content (positional)
/// The optional content above the brace.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct OverbraceNode {
/// The content below the brace.
pub body: Content,
/// The optional content above the brace.
pub annotation: Option<Content>,
}
#[node]
impl OverbraceNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let body = args.expect("body")?;
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
}
impl LayoutMath for OverbraceNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let gap = BRACED_GAP.scaled(ctx);
let body = ctx.layout_row(&self.body)?;
let glyph = GlyphFragment::new(ctx, '⏞');
let brace = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
let mut rows = vec![];
ctx.style(ctx.style.for_superscript());
rows.extend(
self.annotation
.as_ref()
.map(|annotation| ctx.layout_row(annotation))
.transpose()?,
);
ctx.unstyle();
rows.push(brace.into());
rows.push(body);
let last = rows.len() - 1;
ctx.push(stack(ctx, rows, Align::Center, gap, last));
Ok(())
}
}

View File

@ -172,61 +172,20 @@ fn layout(
let mut frame = stack(ctx, rows, align, gap, 0); let mut frame = stack(ctx, rows, align, gap, 0);
let height = frame.height(); let height = frame.height();
let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(frame.height() / 2.0 + axis); frame.set_baseline(frame.height() / 2.0 + axis);
if let Some(left) = left { if let Some(left) = left {
ctx.push(GlyphFragment::new(ctx, left).stretch_vertical(ctx, height, short_fall)); ctx.push(GlyphFragment::new(ctx, left).stretch_vertical(ctx, target, short_fall));
} }
ctx.push(frame); ctx.push(frame);
if let Some(right) = right { if let Some(right) = right {
ctx.push( ctx.push(
GlyphFragment::new(ctx, right).stretch_vertical(ctx, height, short_fall), GlyphFragment::new(ctx, right).stretch_vertical(ctx, target, short_fall),
); );
} }
Ok(()) Ok(())
} }
/// Stack rows on top of each other.
///
/// Add a `gap` between each row and uses the baseline of the `baseline`th
/// row for the whole frame.
pub(super) fn stack(
ctx: &MathContext,
rows: Vec<MathRow>,
align: Align,
gap: Abs,
baseline: usize,
) -> Frame {
let mut width = Abs::zero();
let mut height = rows.len().saturating_sub(1) as f64 * gap;
let points = alignments(&rows);
let rows: Vec<_> =
rows.into_iter().map(|row| row.to_line_frame(ctx, &points)).collect();
for row in &rows {
height += row.height();
width.set_max(row.width());
}
let extra = VERTICAL_PADDING.of(height);
height += extra;
let mut y = extra / 2.0;
let mut frame = Frame::new(Size::new(width, height));
for (i, row) in rows.into_iter().enumerate() {
let x = align.position(width - row.width());
let pos = Point::new(x, y);
if i == baseline {
frame.set_baseline(y + row.baseline());
}
y += row.height() + gap;
frame.push_frame(pos, row);
}
frame
}

View File

@ -5,7 +5,6 @@ mod ctx;
mod accent; mod accent;
mod align; mod align;
mod attach; mod attach;
mod braced;
mod frac; mod frac;
mod fragment; mod fragment;
mod lr; mod lr;
@ -14,6 +13,7 @@ mod op;
mod root; mod root;
mod row; mod row;
mod spacing; mod spacing;
mod stack;
mod stretch; mod stretch;
mod style; mod style;
mod symbols; mod symbols;
@ -21,12 +21,12 @@ mod symbols;
pub use self::accent::*; pub use self::accent::*;
pub use self::align::*; pub use self::align::*;
pub use self::attach::*; pub use self::attach::*;
pub use self::braced::*;
pub use self::frac::*; pub use self::frac::*;
pub use self::lr::*; pub use self::lr::*;
pub use self::matrix::*; pub use self::matrix::*;
pub use self::op::*; pub use self::op::*;
pub use self::root::*; pub use self::root::*;
pub use self::stack::*;
pub use self::style::*; pub use self::style::*;
use ttf_parser::GlyphId; use ttf_parser::GlyphId;
@ -65,8 +65,12 @@ pub fn module(sym: &Module) -> Module {
math.def_func::<ScriptsNode>("scripts"); math.def_func::<ScriptsNode>("scripts");
math.def_func::<LimitsNode>("limits"); math.def_func::<LimitsNode>("limits");
math.def_func::<AccentNode>("accent"); math.def_func::<AccentNode>("accent");
math.def_func::<UnderlineNode>("underline");
math.def_func::<OverlineNode>("overline");
math.def_func::<UnderbraceNode>("underbrace"); math.def_func::<UnderbraceNode>("underbrace");
math.def_func::<OverbraceNode>("overbrace"); math.def_func::<OverbraceNode>("overbrace");
math.def_func::<UnderbracketNode>("underbracket");
math.def_func::<OverbracketNode>("overbracket");
// Fractions and matrix-likes. // Fractions and matrix-likes.
math.def_func::<FracNode>("frac"); math.def_func::<FracNode>("frac");

315
library/src/math/stack.rs Normal file
View File

@ -0,0 +1,315 @@
use super::*;
const LINE_GAP: Em = Em::new(0.15);
const BRACE_GAP: Em = Em::new(0.25);
const BRACKET_GAP: Em = Em::new(0.25);
/// # Underline
/// A horizontal line under content.
///
/// ## Example
/// ```
/// $ underline(1 + 2 + ... + 5) $
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content above the line.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct UnderlineNode(Content);
#[node]
impl UnderlineNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
}
impl LayoutMath for UnderlineNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.0, &None, '\u{305}', LINE_GAP, false)
}
}
/// # Overline
/// A horizontal line over content.
///
/// ## Example
/// ```
/// $ overline(1 + 2 + ... + 5) $
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content below the line.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct OverlineNode(Content);
#[node]
impl OverlineNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
}
impl LayoutMath for OverlineNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.0, &None, '\u{332}', LINE_GAP, true)
}
}
/// # Underbrace
/// A horizontal brace under content, with an optional annotation below.
///
/// ## Example
/// ```
/// $ underbrace(1 + 2 + ... + 5, "numbers") $
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content above the brace.
///
/// - annotation: Content (positional)
/// The optional content below the brace.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct UnderbraceNode {
/// The content above the brace.
pub body: Content,
/// The optional content below the brace.
pub annotation: Option<Content>,
}
#[node]
impl UnderbraceNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let body = args.expect("body")?;
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
}
impl LayoutMath for UnderbraceNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.body, &self.annotation, '⏟', BRACE_GAP, false)
}
}
/// # Overbrace
/// A horizontal brace over content, with an optional annotation above.
///
/// ## Example
/// ```
/// $ overbrace(1 + 2 + ... + 5, "numbers") $
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content below the brace.
///
/// - annotation: Content (positional)
/// The optional content above the brace.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct OverbraceNode {
/// The content below the brace.
pub body: Content,
/// The optional content above the brace.
pub annotation: Option<Content>,
}
#[node]
impl OverbraceNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let body = args.expect("body")?;
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
}
impl LayoutMath for OverbraceNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.body, &self.annotation, '⏞', BRACE_GAP, true)
}
}
/// # Underbracket
/// A horizontal bracket under content, with an optional annotation below.
///
/// ## Example
/// ```
/// $ underbracket(1 + 2 + ... + 5, "numbers") $
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content above the bracket.
///
/// - annotation: Content (positional)
/// The optional content below the bracket.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct UnderbracketNode {
/// The content above the bracket.
pub body: Content,
/// The optional content below the bracket.
pub annotation: Option<Content>,
}
#[node]
impl UnderbracketNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let body = args.expect("body")?;
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
}
impl LayoutMath for UnderbracketNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.body, &self.annotation, '⎵', BRACKET_GAP, false)
}
}
/// # Overbracket
/// A horizontal bracket over content, with an optional annotation above.
///
/// ## Example
/// ```
/// $ overbracket(1 + 2 + ... + 5, "numbers") $
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content below the bracket.
///
/// - annotation: Content (positional)
/// The optional content above the bracket.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct OverbracketNode {
/// The content below the bracket.
pub body: Content,
/// The optional content above the bracket.
pub annotation: Option<Content>,
}
#[node]
impl OverbracketNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let body = args.expect("body")?;
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
}
impl LayoutMath for OverbracketNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.body, &self.annotation, '⎴', BRACKET_GAP, true)
}
}
/// Layout an over- or underthing.
fn layout(
ctx: &mut MathContext,
body: &Content,
annotation: &Option<Content>,
c: char,
gap: Em,
reverse: bool,
) -> SourceResult<()> {
let gap = gap.scaled(ctx);
let body = ctx.layout_row(body)?;
let glyph = GlyphFragment::new(ctx, c);
let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
let mut rows = vec![body, stretched.into()];
ctx.style(if reverse {
ctx.style.for_subscript()
} else {
ctx.style.for_superscript()
});
rows.extend(
annotation
.as_ref()
.map(|annotation| ctx.layout_row(annotation))
.transpose()?,
);
ctx.unstyle();
let mut baseline = 0;
if reverse {
rows.reverse();
baseline = rows.len() - 1;
}
ctx.push(stack(ctx, rows, Align::Center, gap, baseline));
Ok(())
}
/// Stack rows on top of each other.
///
/// Add a `gap` between each row and uses the baseline of the `baseline`th
/// row for the whole frame.
pub(super) fn stack(
ctx: &MathContext,
rows: Vec<MathRow>,
align: Align,
gap: Abs,
baseline: usize,
) -> Frame {
let mut width = Abs::zero();
let mut height = rows.len().saturating_sub(1) as f64 * gap;
let points = alignments(&rows);
let rows: Vec<_> =
rows.into_iter().map(|row| row.to_line_frame(ctx, &points)).collect();
for row in &rows {
height += row.height();
width.set_max(row.width());
}
let mut y = Abs::zero();
let mut frame = Frame::new(Size::new(width, height));
for (i, row) in rows.into_iter().enumerate() {
let x = align.position(width - row.width());
let pos = Point::new(x, y);
if i == baseline {
frame.set_baseline(y + row.baseline());
}
y += row.height() + gap;
frame.push_frame(pos, row);
}
frame
}

View File

@ -150,13 +150,11 @@ symbols! {
breve: '˘', breve: '˘',
caret: '', caret: '',
caron: 'ˇ', caron: 'ˇ',
cedilla: '¸',
circum: '^', circum: '^',
diaer: '¨', diaer: '¨',
grave: '`', grave: '`',
macron: '¯', macron: '¯',
tilde: '~', tilde: '~',
overline: '',
// Currency. // Currency.
bitcoin: '', bitcoin: '',

View File

@ -174,12 +174,8 @@ pub fn combining_accent(c: char) -> Option<char> {
'\u{030a}' | '∘' | '○' => '\u{030a}', '\u{030a}' | '∘' | '○' => '\u{030a}',
'\u{030b}' | '˝' => '\u{030b}', '\u{030b}' | '˝' => '\u{030b}',
'\u{030c}' | 'ˇ' => '\u{030c}', '\u{030c}' | 'ˇ' => '\u{030c}',
'\u{0327}' | '¸' => '\u{0327}',
'\u{0328}' | '˛' => '\u{0328}',
'\u{0332}' | '_' => '\u{0332}',
'\u{20d6}' | '←' => '\u{20d6}', '\u{20d6}' | '←' => '\u{20d6}',
'\u{20d7}' | '→' | '⟶' => '\u{20d7}', '\u{20d7}' | '→' | '⟶' => '\u{20d7}',
'⏞' | '⏟' | '⎴' | '⎵' | '⏜' | '⏝' | '⏠' | '⏡' => c,
_ => return None, _ => return None,
}) })
} }

View File

@ -181,7 +181,7 @@ impl Lexer<'_> {
'*' if !self.in_word() => SyntaxKind::Star, '*' if !self.in_word() => SyntaxKind::Star,
'_' if !self.in_word() => SyntaxKind::Underscore, '_' if !self.in_word() => SyntaxKind::Underscore,
'#' if !self.s.at(char::is_whitespace) => SyntaxKind::Hashtag, '#' if self.s.at(|c: char| !c.is_whitespace()) => SyntaxKind::Hashtag,
'[' => SyntaxKind::LeftBracket, '[' => SyntaxKind::LeftBracket,
']' => SyntaxKind::RightBracket, ']' => SyntaxKind::RightBracket,
'\'' => SyntaxKind::SmartQuote, '\'' => SyntaxKind::SmartQuote,
@ -389,7 +389,6 @@ impl Lexer<'_> {
'<' if self.s.eat_if('<') => SyntaxKind::Shorthand, '<' if self.s.eat_if('<') => SyntaxKind::Shorthand,
'>' if self.s.eat_if(">>") => SyntaxKind::Shorthand, '>' if self.s.eat_if(">>") => SyntaxKind::Shorthand,
'>' if self.s.eat_if('>') => SyntaxKind::Shorthand, '>' if self.s.eat_if('>') => SyntaxKind::Shorthand,
'<' if self.s.eat_if("=>") => SyntaxKind::Shorthand, '<' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
'<' if self.s.eat_if("->") => SyntaxKind::Shorthand, '<' if self.s.eat_if("->") => SyntaxKind::Shorthand,
'<' if self.s.eat_if('=') => SyntaxKind::Shorthand, '<' if self.s.eat_if('=') => SyntaxKind::Shorthand,
@ -397,13 +396,12 @@ impl Lexer<'_> {
'<' if self.s.eat_if('-') => SyntaxKind::Shorthand, '<' if self.s.eat_if('-') => SyntaxKind::Shorthand,
'-' if self.s.eat_if('>') => SyntaxKind::Shorthand, '-' if self.s.eat_if('>') => SyntaxKind::Shorthand,
'=' if self.s.eat_if('>') => SyntaxKind::Shorthand, '=' if self.s.eat_if('>') => SyntaxKind::Shorthand,
':' if self.s.eat_if('=') => SyntaxKind::Shorthand, ':' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'[' if self.s.eat_if('|') => SyntaxKind::Shorthand, '[' if self.s.eat_if('|') => SyntaxKind::Shorthand,
'|' if self.s.eat_if(']') => SyntaxKind::Shorthand, '|' if self.s.eat_if(']') => SyntaxKind::Shorthand,
'|' if self.s.eat_if('|') => SyntaxKind::Shorthand, '|' if self.s.eat_if('|') => SyntaxKind::Shorthand,
'#' if !self.s.at(char::is_whitespace) => SyntaxKind::Hashtag, '#' if self.s.at(|c: char| !c.is_whitespace()) => SyntaxKind::Hashtag,
'_' => SyntaxKind::Underscore, '_' => SyntaxKind::Underscore,
'$' => SyntaxKind::Dollar, '$' => SyntaxKind::Dollar,
'/' => SyntaxKind::Slash, '/' => SyntaxKind::Slash,