Configurable font edges ⚙
Adds top-edge and bottom-edge parameters to the font function. These define how the box around a word is computed. The possible values are: - ascender - cap-height (default top edge) - x-height - baseline (default bottom edge) - descender The defaults are chosen so that it's easy to create good-looking designs with vertical alignment. Since they are much tighter than what most other software uses by default, the default leading had to be increased to 50% of the font size and paragraph spacing to 100% of the font size. The values cap-height and x-height fall back to ascender in case they are zero because this value may occur in fonts that don't have glyphs with cap- or x-height (like Twitter Color Emoji). Since cap-height is the default top edge, doing no fallback would break things badly. Removes softness in favor of a simple boolean for pages and a more finegread u8 for spacing. This is needed to make paragraph spacing consume line spacing created by hard line breaks.
@ -38,7 +38,7 @@ impl<'a> ExecContext<'a> {
|
|||||||
env,
|
env,
|
||||||
diags: DiagSet::new(),
|
diags: DiagSet::new(),
|
||||||
tree: Tree { runs: vec![] },
|
tree: Tree { runs: vec![] },
|
||||||
page: Some(PageInfo::new(&state, Softness::Hard)),
|
page: Some(PageInfo::new(&state, true)),
|
||||||
stack: NodeStack::new(&state),
|
stack: NodeStack::new(&state),
|
||||||
par: NodePar::new(&state),
|
par: NodePar::new(&state),
|
||||||
state,
|
state,
|
||||||
@ -77,7 +77,8 @@ impl<'a> ExecContext<'a> {
|
|||||||
|
|
||||||
/// Push a layout node into the active paragraph.
|
/// Push a layout node into the active paragraph.
|
||||||
///
|
///
|
||||||
/// Spacing nodes will be handled according to their [`Softness`].
|
/// Spacing nodes will be handled according to their
|
||||||
|
/// [`softness`](NodeSpacing::softness).
|
||||||
pub fn push(&mut self, node: impl Into<Node>) {
|
pub fn push(&mut self, node: impl Into<Node>) {
|
||||||
push(&mut self.par.children, node.into());
|
push(&mut self.par.children, node.into());
|
||||||
}
|
}
|
||||||
@ -87,7 +88,7 @@ impl<'a> ExecContext<'a> {
|
|||||||
let em = self.state.font.font_size();
|
let em = self.state.font.font_size();
|
||||||
self.push(NodeSpacing {
|
self.push(NodeSpacing {
|
||||||
amount: self.state.par.word_spacing.resolve(em),
|
amount: self.state.par.word_spacing.resolve(em),
|
||||||
softness: Softness::Soft,
|
softness: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,15 +110,19 @@ impl<'a> ExecContext<'a> {
|
|||||||
|
|
||||||
/// Apply a forced line break.
|
/// Apply a forced line break.
|
||||||
pub fn push_linebreak(&mut self) {
|
pub fn push_linebreak(&mut self) {
|
||||||
self.finish_par();
|
let em = self.state.font.font_size();
|
||||||
|
self.push_into_stack(NodeSpacing {
|
||||||
|
amount: self.state.par.leading.resolve(em),
|
||||||
|
softness: 2,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a forced paragraph break.
|
/// Apply a forced paragraph break.
|
||||||
pub fn push_parbreak(&mut self) {
|
pub fn push_parbreak(&mut self) {
|
||||||
let em = self.state.font.font_size();
|
let em = self.state.font.font_size();
|
||||||
self.push_into_stack(NodeSpacing {
|
self.push_into_stack(NodeSpacing {
|
||||||
amount: self.state.par.par_spacing.resolve(em),
|
amount: self.state.par.spacing.resolve(em),
|
||||||
softness: Softness::Soft,
|
softness: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,11 +168,13 @@ impl<'a> ExecContext<'a> {
|
|||||||
|
|
||||||
NodeText {
|
NodeText {
|
||||||
text,
|
text,
|
||||||
aligns: self.state.aligns,
|
|
||||||
dir: self.state.dirs.cross,
|
dir: self.state.dirs.cross,
|
||||||
font_size: self.state.font.font_size(),
|
aligns: self.state.aligns,
|
||||||
families: Rc::clone(&self.state.font.families),
|
families: Rc::clone(&self.state.font.families),
|
||||||
variant,
|
variant,
|
||||||
|
font_size: self.state.font.font_size(),
|
||||||
|
top_edge: self.state.font.top_edge,
|
||||||
|
bottom_edge: self.state.font.bottom_edge,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,12 +199,12 @@ impl<'a> ExecContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Finish the active page.
|
/// Finish the active page.
|
||||||
pub fn finish_page(&mut self, keep: bool, new_softness: Softness, source: Span) {
|
pub fn finish_page(&mut self, keep: bool, hard: bool, source: Span) {
|
||||||
if let Some(info) = &mut self.page {
|
if let Some(info) = &mut self.page {
|
||||||
let info = mem::replace(info, PageInfo::new(&self.state, new_softness));
|
let info = mem::replace(info, PageInfo::new(&self.state, hard));
|
||||||
let stack = self.finish_stack();
|
let stack = self.finish_stack();
|
||||||
|
|
||||||
if !stack.children.is_empty() || (keep && info.softness == Softness::Hard) {
|
if !stack.children.is_empty() || (keep && info.hard) {
|
||||||
self.tree.runs.push(NodePages {
|
self.tree.runs.push(NodePages {
|
||||||
size: info.size,
|
size: info.size,
|
||||||
child: NodePad {
|
child: NodePad {
|
||||||
@ -215,7 +222,7 @@ impl<'a> ExecContext<'a> {
|
|||||||
/// Finish execution and return the created layout tree.
|
/// Finish execution and return the created layout tree.
|
||||||
pub fn finish(mut self) -> Pass<Tree> {
|
pub fn finish(mut self) -> Pass<Tree> {
|
||||||
assert!(self.page.is_some());
|
assert!(self.page.is_some());
|
||||||
self.finish_page(true, Softness::Soft, Span::default());
|
self.finish_page(true, false, Span::default());
|
||||||
Pass::new(self.tree, self.diags)
|
Pass::new(self.tree, self.diags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,16 +230,18 @@ impl<'a> ExecContext<'a> {
|
|||||||
/// Push a node into a list, taking care of spacing softness.
|
/// Push a node into a list, taking care of spacing softness.
|
||||||
fn push(nodes: &mut Vec<Node>, node: Node) {
|
fn push(nodes: &mut Vec<Node>, node: Node) {
|
||||||
if let Node::Spacing(spacing) = node {
|
if let Node::Spacing(spacing) = node {
|
||||||
if spacing.softness == Softness::Soft && nodes.is_empty() {
|
if nodes.is_empty() && spacing.softness > 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(&Node::Spacing(other)) = nodes.last() {
|
if let Some(&Node::Spacing(other)) = nodes.last() {
|
||||||
if spacing.softness > other.softness {
|
if spacing.softness > 0 && spacing.softness >= other.softness {
|
||||||
nodes.pop();
|
|
||||||
} else if spacing.softness == Softness::Soft {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if spacing.softness < other.softness {
|
||||||
|
nodes.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +251,7 @@ fn push(nodes: &mut Vec<Node>, node: Node) {
|
|||||||
/// Remove trailing soft spacing from a node list.
|
/// Remove trailing soft spacing from a node list.
|
||||||
fn trim(nodes: &mut Vec<Node>) {
|
fn trim(nodes: &mut Vec<Node>) {
|
||||||
if let Some(&Node::Spacing(spacing)) = nodes.last() {
|
if let Some(&Node::Spacing(spacing)) = nodes.last() {
|
||||||
if spacing.softness == Softness::Soft {
|
if spacing.softness > 0 {
|
||||||
nodes.pop();
|
nodes.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,15 +261,15 @@ fn trim(nodes: &mut Vec<Node>) {
|
|||||||
struct PageInfo {
|
struct PageInfo {
|
||||||
size: Size,
|
size: Size,
|
||||||
padding: Sides<Linear>,
|
padding: Sides<Linear>,
|
||||||
softness: Softness,
|
hard: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PageInfo {
|
impl PageInfo {
|
||||||
fn new(state: &State, softness: Softness) -> Self {
|
fn new(state: &State, hard: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
size: state.page.size,
|
size: state.page.size,
|
||||||
padding: state.page.margins(),
|
padding: state.page.margins(),
|
||||||
softness,
|
hard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,7 +290,7 @@ impl NodePar {
|
|||||||
Self {
|
Self {
|
||||||
dirs: state.dirs,
|
dirs: state.dirs,
|
||||||
aligns: state.aligns,
|
aligns: state.aligns,
|
||||||
line_spacing: state.par.line_spacing.resolve(em),
|
line_spacing: state.par.leading.resolve(em),
|
||||||
children: vec![],
|
children: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,15 +35,6 @@ pub fn exec(
|
|||||||
ctx.finish()
|
ctx.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Defines how an item interacts with surrounding items.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
|
||||||
pub enum Softness {
|
|
||||||
/// A soft item can be skipped in some circumstances.
|
|
||||||
Soft,
|
|
||||||
/// A hard item is always retained.
|
|
||||||
Hard,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a node.
|
/// Execute a node.
|
||||||
///
|
///
|
||||||
/// This manipulates active styling and document state and produces layout
|
/// This manipulates active styling and document state and produces layout
|
||||||
@ -106,15 +97,15 @@ impl Exec for NodeRaw {
|
|||||||
ctx.set_monospace();
|
ctx.set_monospace();
|
||||||
|
|
||||||
let em = ctx.state.font.font_size();
|
let em = ctx.state.font.font_size();
|
||||||
let line_spacing = ctx.state.par.line_spacing.resolve(em);
|
let leading = ctx.state.par.leading.resolve(em);
|
||||||
|
|
||||||
let mut children = vec![];
|
let mut children = vec![];
|
||||||
let mut newline = false;
|
let mut newline = false;
|
||||||
for line in &self.lines {
|
for line in &self.lines {
|
||||||
if newline {
|
if newline {
|
||||||
children.push(layout::Node::Spacing(NodeSpacing {
|
children.push(layout::Node::Spacing(NodeSpacing {
|
||||||
amount: line_spacing,
|
amount: leading,
|
||||||
softness: Softness::Soft,
|
softness: 2,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,9 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight};
|
use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight};
|
||||||
|
|
||||||
use crate::geom::{
|
use crate::geom::*;
|
||||||
Align, Dir, LayoutAligns, LayoutDirs, Length, Linear, Relative, Sides, Size,
|
|
||||||
};
|
|
||||||
use crate::paper::{Paper, PaperClass, PAPER_A4};
|
use crate::paper::{Paper, PaperClass, PAPER_A4};
|
||||||
|
use crate::shaping::VerticalFontMetric;
|
||||||
|
|
||||||
/// The evaluation state.
|
/// The evaluation state.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@ -77,20 +76,20 @@ impl Default for PageState {
|
|||||||
/// Defines paragraph properties.
|
/// Defines paragraph properties.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub struct ParState {
|
pub struct ParState {
|
||||||
|
/// The spacing between paragraphs (dependent on scaled font size).
|
||||||
|
pub spacing: Linear,
|
||||||
|
/// The spacing between lines (dependent on scaled font size).
|
||||||
|
pub leading: Linear,
|
||||||
/// The spacing between words (dependent on scaled font size).
|
/// The spacing between words (dependent on scaled font size).
|
||||||
pub word_spacing: Linear,
|
pub word_spacing: Linear,
|
||||||
/// The spacing between lines (dependent on scaled font size).
|
|
||||||
pub line_spacing: Linear,
|
|
||||||
/// The spacing between paragraphs (dependent on scaled font size).
|
|
||||||
pub par_spacing: Linear,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ParState {
|
impl Default for ParState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
spacing: Relative::new(1.0).into(),
|
||||||
|
leading: Relative::new(0.5).into(),
|
||||||
word_spacing: Relative::new(0.25).into(),
|
word_spacing: Relative::new(0.25).into(),
|
||||||
line_spacing: Linear::ZERO,
|
|
||||||
par_spacing: Relative::new(0.5).into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,6 +105,10 @@ pub struct FontState {
|
|||||||
pub size: Length,
|
pub size: Length,
|
||||||
/// The linear to apply on the base font size.
|
/// The linear to apply on the base font size.
|
||||||
pub scale: Linear,
|
pub scale: Linear,
|
||||||
|
/// The top end of the text bounding box.
|
||||||
|
pub top_edge: VerticalFontMetric,
|
||||||
|
/// The bottom end of the text bounding box.
|
||||||
|
pub bottom_edge: VerticalFontMetric,
|
||||||
/// Whether the strong toggle is active or inactive. This determines
|
/// Whether the strong toggle is active or inactive. This determines
|
||||||
/// whether the next `*` adds or removes font weight.
|
/// whether the next `*` adds or removes font weight.
|
||||||
pub strong: bool,
|
pub strong: bool,
|
||||||
@ -141,6 +144,8 @@ impl Default for FontState {
|
|||||||
stretch: FontStretch::Normal,
|
stretch: FontStretch::Normal,
|
||||||
},
|
},
|
||||||
size: Length::pt(11.0),
|
size: Length::pt(11.0),
|
||||||
|
top_edge: VerticalFontMetric::CapHeight,
|
||||||
|
bottom_edge: VerticalFontMetric::Baseline,
|
||||||
scale: Linear::ONE,
|
scale: Linear::ONE,
|
||||||
strong: false,
|
strong: false,
|
||||||
emph: false,
|
emph: false,
|
||||||
|
@ -180,6 +180,7 @@ impl<'a> PdfExporter<'a> {
|
|||||||
|
|
||||||
Element::Text(shaped) => {
|
Element::Text(shaped) => {
|
||||||
let mut text = content.text();
|
let mut text = content.text();
|
||||||
|
|
||||||
// Check if we need to issue a font switching action.
|
// Check if we need to issue a font switching action.
|
||||||
if shaped.face != face || shaped.font_size != size {
|
if shaped.face != face || shaped.font_size != size {
|
||||||
face = shaped.face;
|
face = shaped.face;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::exec::Softness;
|
|
||||||
|
|
||||||
/// A spacing node.
|
/// A spacing node.
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
@ -10,13 +9,11 @@ pub struct NodeSpacing {
|
|||||||
pub amount: Length,
|
pub amount: Length,
|
||||||
/// Defines how spacing interacts with surrounding spacing.
|
/// Defines how spacing interacts with surrounding spacing.
|
||||||
///
|
///
|
||||||
/// Hard spacing assures that a fixed amount of spacing will always be
|
/// Hard spacing (`softness = 0`) assures that a fixed amount of spacing
|
||||||
/// inserted. Soft spacing will be consumed by previous soft spacing or
|
/// will always be inserted. Soft spacing (`softness >= 1`) will be consumed
|
||||||
/// neighbouring hard spacing and can be used to insert overridable spacing,
|
/// by other spacing with lower softness and can be used to insert
|
||||||
/// e.g. between words or paragraphs.
|
/// overridable spacing, e.g. between words or paragraphs.
|
||||||
///
|
pub softness: u8,
|
||||||
/// This field is only used in evaluation, not in layouting.
|
|
||||||
pub softness: Softness,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout for NodeSpacing {
|
impl Layout for NodeSpacing {
|
||||||
@ -27,10 +24,7 @@ impl Layout for NodeSpacing {
|
|||||||
|
|
||||||
impl Debug for NodeSpacing {
|
impl Debug for NodeSpacing {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self.softness {
|
write!(f, "Spacing({}, {})", self.amount, self.softness)
|
||||||
Softness::Soft => write!(f, "Soft({})", self.amount),
|
|
||||||
Softness::Hard => write!(f, "Hard({})", self.amount),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,35 +4,41 @@ use std::rc::Rc;
|
|||||||
use fontdock::{FallbackTree, FontVariant};
|
use fontdock::{FallbackTree, FontVariant};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::shaping;
|
use crate::shaping::{shape, VerticalFontMetric};
|
||||||
|
|
||||||
/// A text node.
|
/// A text node.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct NodeText {
|
pub struct NodeText {
|
||||||
|
/// The text.
|
||||||
|
pub text: String,
|
||||||
/// The text direction.
|
/// The text direction.
|
||||||
pub dir: Dir,
|
pub dir: Dir,
|
||||||
/// How to align this text node in its parent.
|
/// How to align this text node in its parent.
|
||||||
pub aligns: LayoutAligns,
|
pub aligns: LayoutAligns,
|
||||||
/// The text.
|
|
||||||
pub text: String,
|
|
||||||
/// The font size.
|
|
||||||
pub font_size: Length,
|
|
||||||
/// The families used for font fallback.
|
/// The families used for font fallback.
|
||||||
pub families: Rc<FallbackTree>,
|
pub families: Rc<FallbackTree>,
|
||||||
/// The font variant,
|
/// The font variant,
|
||||||
pub variant: FontVariant,
|
pub variant: FontVariant,
|
||||||
|
/// The font size.
|
||||||
|
pub font_size: Length,
|
||||||
|
/// The top end of the text bounding box.
|
||||||
|
pub top_edge: VerticalFontMetric,
|
||||||
|
/// The bottom end of the text bounding box.
|
||||||
|
pub bottom_edge: VerticalFontMetric,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout for NodeText {
|
impl Layout for NodeText {
|
||||||
fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Layouted {
|
fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Layouted {
|
||||||
Layouted::Frame(
|
Layouted::Frame(
|
||||||
shaping::shape(
|
shape(
|
||||||
&self.text,
|
&self.text,
|
||||||
self.dir,
|
self.dir,
|
||||||
self.font_size,
|
|
||||||
&mut ctx.env.fonts,
|
|
||||||
&self.families,
|
&self.families,
|
||||||
self.variant,
|
self.variant,
|
||||||
|
self.font_size,
|
||||||
|
self.top_edge,
|
||||||
|
self.bottom_edge,
|
||||||
|
&mut ctx.env.fonts,
|
||||||
),
|
),
|
||||||
self.aligns,
|
self.aligns,
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use fontdock::{FontStretch, FontStyle, FontWeight};
|
use fontdock::{FontStretch, FontStyle, FontWeight};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::shaping::VerticalFontMetric;
|
||||||
|
|
||||||
/// `font`: Configure the font.
|
/// `font`: Configure the font.
|
||||||
///
|
///
|
||||||
@ -13,6 +14,8 @@ use super::*;
|
|||||||
/// - Font Style: `style`, of type `font-style`.
|
/// - Font Style: `style`, of type `font-style`.
|
||||||
/// - Font Weight: `weight`, of type `font-weight`.
|
/// - Font Weight: `weight`, of type `font-weight`.
|
||||||
/// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0.
|
/// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0.
|
||||||
|
/// - Top edge of the font: `top-edge`, of type `vertical-font-metric`.
|
||||||
|
/// - Bottom edge of the font: `bottom-edge`, of type `vertical-font-metric`.
|
||||||
/// - Serif family definition: `serif`, of type `font-familiy-list`.
|
/// - Serif family definition: `serif`, of type `font-familiy-list`.
|
||||||
/// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`.
|
/// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`.
|
||||||
/// - Monospace family definition: `monospace`, of type `font-familiy-list`.
|
/// - Monospace family definition: `monospace`, of type `font-familiy-list`.
|
||||||
@ -22,15 +25,15 @@ use super::*;
|
|||||||
/// if present.
|
/// if present.
|
||||||
///
|
///
|
||||||
/// # Relevant types and constants
|
/// # Relevant types and constants
|
||||||
/// - Type `font-family-list`
|
|
||||||
/// - coerces from `string`
|
|
||||||
/// - coerces from `array`
|
|
||||||
/// - coerces from `font-family`
|
|
||||||
/// - Type `font-family`
|
/// - Type `font-family`
|
||||||
/// - `serif`
|
/// - `serif`
|
||||||
/// - `sans-serif`
|
/// - `sans-serif`
|
||||||
/// - `monospace`
|
/// - `monospace`
|
||||||
/// - coerces from `string`
|
/// - coerces from `string`
|
||||||
|
/// - Type `font-family-list`
|
||||||
|
/// - coerces from `string`
|
||||||
|
/// - coerces from `array`
|
||||||
|
/// - coerces from `font-family`
|
||||||
/// - Type `font-style`
|
/// - Type `font-style`
|
||||||
/// - `normal`
|
/// - `normal`
|
||||||
/// - `italic`
|
/// - `italic`
|
||||||
@ -46,12 +49,20 @@ use super::*;
|
|||||||
/// - `extrabold` (800)
|
/// - `extrabold` (800)
|
||||||
/// - `black` (900)
|
/// - `black` (900)
|
||||||
/// - coerces from `integer`
|
/// - coerces from `integer`
|
||||||
|
/// - Type `vertical-font-metric`
|
||||||
|
/// - `ascender`
|
||||||
|
/// - `cap-height`
|
||||||
|
/// - `x-height`
|
||||||
|
/// - `baseline`
|
||||||
|
/// - `descender`
|
||||||
pub fn font(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
|
pub fn font(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
|
||||||
let size = args.find::<Linear>(ctx);
|
let size = args.find::<Linear>(ctx);
|
||||||
let list: Vec<_> = args.filter::<FontFamily>(ctx).map(|f| f.to_string()).collect();
|
let list: Vec<_> = args.filter::<FontFamily>(ctx).map(|f| f.to_string()).collect();
|
||||||
let style = args.get(ctx, "style");
|
let style = args.get(ctx, "style");
|
||||||
let weight = args.get(ctx, "weight");
|
let weight = args.get(ctx, "weight");
|
||||||
let stretch = args.get(ctx, "stretch");
|
let stretch = args.get(ctx, "stretch");
|
||||||
|
let top_edge = args.get(ctx, "top-edge");
|
||||||
|
let bottom_edge = args.get(ctx, "bottom-edge");
|
||||||
let serif = args.get(ctx, "serif");
|
let serif = args.get(ctx, "serif");
|
||||||
let sans_serif = args.get(ctx, "sans-serif");
|
let sans_serif = args.get(ctx, "sans-serif");
|
||||||
let monospace = args.get(ctx, "monospace");
|
let monospace = args.get(ctx, "monospace");
|
||||||
@ -87,6 +98,14 @@ pub fn font(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
|
|||||||
ctx.state.font.variant.stretch = stretch;
|
ctx.state.font.variant.stretch = stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(top_edge) = top_edge {
|
||||||
|
ctx.state.font.top_edge = top_edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bottom_edge) = bottom_edge {
|
||||||
|
ctx.state.font.bottom_edge = bottom_edge;
|
||||||
|
}
|
||||||
|
|
||||||
for (variant, arg) in &[
|
for (variant, arg) in &[
|
||||||
(FontFamily::Serif, &serif),
|
(FontFamily::Serif, &serif),
|
||||||
(FontFamily::SansSerif, &sans_serif),
|
(FontFamily::SansSerif, &sans_serif),
|
||||||
@ -185,3 +204,7 @@ typify! {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typify! {
|
||||||
|
VerticalFontMetric: "vertical font metric",
|
||||||
|
}
|
||||||
|
@ -26,9 +26,9 @@ use std::fmt::{self, Display, Formatter};
|
|||||||
use fontdock::{FontStyle, FontWeight};
|
use fontdock::{FontStyle, FontWeight};
|
||||||
|
|
||||||
use crate::eval::{Scope, ValueAny, ValueFunc};
|
use crate::eval::{Scope, ValueAny, ValueFunc};
|
||||||
use crate::exec::Softness;
|
|
||||||
use crate::layout::*;
|
use crate::layout::*;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::shaping::VerticalFontMetric;
|
||||||
|
|
||||||
/// Construct a scope containing all standard library definitions.
|
/// Construct a scope containing all standard library definitions.
|
||||||
pub fn new() -> Scope {
|
pub fn new() -> Scope {
|
||||||
@ -81,6 +81,11 @@ pub fn new() -> Scope {
|
|||||||
set!(any: "bold", FontWeight::BOLD);
|
set!(any: "bold", FontWeight::BOLD);
|
||||||
set!(any: "extrabold", FontWeight::EXTRABOLD);
|
set!(any: "extrabold", FontWeight::EXTRABOLD);
|
||||||
set!(any: "black", FontWeight::BLACK);
|
set!(any: "black", FontWeight::BLACK);
|
||||||
|
set!(any: "ascender", VerticalFontMetric::Ascender);
|
||||||
|
set!(any: "cap-height", VerticalFontMetric::CapHeight);
|
||||||
|
set!(any: "x-height", VerticalFontMetric::XHeight);
|
||||||
|
set!(any: "baseline", VerticalFontMetric::Baseline);
|
||||||
|
set!(any: "descender", VerticalFontMetric::Descender);
|
||||||
|
|
||||||
std
|
std
|
||||||
}
|
}
|
||||||
|
@ -95,13 +95,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.set_dirs(Gen::new(main, cross));
|
ctx.set_dirs(Gen::new(main, cross));
|
||||||
ctx.finish_page(false, Softness::Hard, span);
|
ctx.finish_page(false, true, span);
|
||||||
|
|
||||||
if let Some(body) = &body {
|
if let Some(body) = &body {
|
||||||
// TODO: Restrict body to a single page?
|
// TODO: Restrict body to a single page?
|
||||||
body.exec(ctx);
|
body.exec(ctx);
|
||||||
ctx.state = snapshot;
|
ctx.state = snapshot;
|
||||||
ctx.finish_page(true, Softness::Soft, span);
|
ctx.finish_page(true, false, span);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -113,6 +113,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
|
|||||||
pub fn pagebreak(_: &mut EvalContext, args: &mut ValueArgs) -> Value {
|
pub fn pagebreak(_: &mut EvalContext, args: &mut ValueArgs) -> Value {
|
||||||
let span = args.span;
|
let span = args.span;
|
||||||
Value::template("pagebreak", move |ctx| {
|
Value::template("pagebreak", move |ctx| {
|
||||||
ctx.finish_page(true, Softness::Hard, span);
|
ctx.finish_page(true, true, span);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ fn spacing(ctx: &mut EvalContext, args: &mut ValueArgs, axis: SpecAxis) -> Value
|
|||||||
Value::template("spacing", move |ctx| {
|
Value::template("spacing", move |ctx| {
|
||||||
if let Some(linear) = spacing {
|
if let Some(linear) = spacing {
|
||||||
let amount = linear.resolve(ctx.state.font.font_size());
|
let amount = linear.resolve(ctx.state.font.font_size());
|
||||||
let spacing = NodeSpacing { amount, softness: Softness::Hard };
|
let spacing = NodeSpacing { amount, softness: 0 };
|
||||||
if axis == ctx.state.dirs.main.axis() {
|
if axis == ctx.state.dirs.main.axis() {
|
||||||
ctx.push_into_stack(spacing);
|
ctx.push_into_stack(spacing);
|
||||||
} else {
|
} else {
|
||||||
|
115
src/shaping.rs
@ -4,7 +4,7 @@
|
|||||||
//! font for each individual character. When the direction is right-to-left, the
|
//! font for each individual character. When the direction is right-to-left, the
|
||||||
//! word is spelled backwards. Vertical shaping is not supported.
|
//! word is spelled backwards. Vertical shaping is not supported.
|
||||||
|
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
|
|
||||||
use fontdock::{FaceId, FaceQuery, FallbackTree, FontVariant};
|
use fontdock::{FaceId, FaceQuery, FallbackTree, FontVariant};
|
||||||
use ttf_parser::{Face, GlyphId};
|
use ttf_parser::{Face, GlyphId};
|
||||||
@ -58,20 +58,55 @@ impl Debug for Shaped {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Identifies a vertical metric of a font.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub enum VerticalFontMetric {
|
||||||
|
/// The distance from the baseline to the typographic ascender.
|
||||||
|
///
|
||||||
|
/// Corresponds to the typographic ascender from the `OS/2` table if present
|
||||||
|
/// and falls back to the ascender from the `hhea` table otherwise.
|
||||||
|
Ascender,
|
||||||
|
/// The approximate height of uppercase letters.
|
||||||
|
CapHeight,
|
||||||
|
/// The approximate height of non-ascending lowercase letters.
|
||||||
|
XHeight,
|
||||||
|
/// The baseline on which the letters rest.
|
||||||
|
Baseline,
|
||||||
|
/// The distance from the baseline to the typographic descender.
|
||||||
|
///
|
||||||
|
/// Corresponds to the typographic descender from the `OS/2` table if
|
||||||
|
/// present and falls back to the descender from the `hhea` table otherwise.
|
||||||
|
Descender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for VerticalFontMetric {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
f.pad(match self {
|
||||||
|
Self::Ascender => "ascender",
|
||||||
|
Self::CapHeight => "cap-height",
|
||||||
|
Self::XHeight => "x-height",
|
||||||
|
Self::Baseline => "baseline",
|
||||||
|
Self::Descender => "descender",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shape text into a frame containing [`Shaped`] runs.
|
/// Shape text into a frame containing [`Shaped`] runs.
|
||||||
pub fn shape(
|
pub fn shape(
|
||||||
text: &str,
|
text: &str,
|
||||||
dir: Dir,
|
dir: Dir,
|
||||||
font_size: Length,
|
|
||||||
loader: &mut FontLoader,
|
|
||||||
fallback: &FallbackTree,
|
fallback: &FallbackTree,
|
||||||
variant: FontVariant,
|
variant: FontVariant,
|
||||||
|
font_size: Length,
|
||||||
|
top_edge: VerticalFontMetric,
|
||||||
|
bottom_edge: VerticalFontMetric,
|
||||||
|
loader: &mut FontLoader,
|
||||||
) -> Frame {
|
) -> Frame {
|
||||||
let mut frame = Frame::new(Size::new(Length::ZERO, font_size));
|
let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
|
||||||
let mut shaped = Shaped::new(FaceId::MAX, font_size);
|
let mut shaped = Shaped::new(FaceId::MAX, font_size);
|
||||||
let mut offset = Length::ZERO;
|
let mut width = Length::ZERO;
|
||||||
let mut ascender = Length::ZERO;
|
let mut top = Length::ZERO;
|
||||||
let mut descender = Length::ZERO;
|
let mut bottom = Length::ZERO;
|
||||||
|
|
||||||
// Create an iterator with conditional direction.
|
// Create an iterator with conditional direction.
|
||||||
let mut forwards = text.chars();
|
let mut forwards = text.chars();
|
||||||
@ -86,7 +121,7 @@ pub fn shape(
|
|||||||
let query = FaceQuery { fallback: fallback.iter(), variant, c };
|
let query = FaceQuery { fallback: fallback.iter(), variant, c };
|
||||||
if let Some(id) = loader.query(query) {
|
if let Some(id) = loader.query(query) {
|
||||||
let face = loader.face(id).get();
|
let face = loader.face(id).get();
|
||||||
let (glyph, width) = match lookup_glyph(face, c) {
|
let (glyph, glyph_width) = match lookup_glyph(face, c) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
@ -96,27 +131,35 @@ pub fn shape(
|
|||||||
|
|
||||||
// Flush the buffer and reset the metrics if we use a new font face.
|
// Flush the buffer and reset the metrics if we use a new font face.
|
||||||
if shaped.face != id {
|
if shaped.face != id {
|
||||||
place(&mut frame, shaped, offset, ascender, descender);
|
place(&mut frame, shaped, width, top, bottom);
|
||||||
|
|
||||||
shaped = Shaped::new(id, font_size);
|
shaped = Shaped::new(id, font_size);
|
||||||
offset = Length::ZERO;
|
width = Length::ZERO;
|
||||||
ascender = convert(f64::from(face.ascender()));
|
top = convert(f64::from(lookup_metric(face, top_edge)));
|
||||||
descender = convert(f64::from(face.descender()));
|
bottom = convert(f64::from(lookup_metric(face, bottom_edge)));
|
||||||
}
|
}
|
||||||
|
|
||||||
shaped.text.push(c);
|
shaped.text.push(c);
|
||||||
shaped.glyphs.push(glyph);
|
shaped.glyphs.push(glyph);
|
||||||
shaped.offsets.push(offset);
|
shaped.offsets.push(width);
|
||||||
offset += convert(f64::from(width));
|
width += convert(f64::from(glyph_width));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush the last buffered parts of the word.
|
place(&mut frame, shaped, width, top, bottom);
|
||||||
place(&mut frame, shaped, offset, ascender, descender);
|
|
||||||
|
|
||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Place shaped text into a frame.
|
||||||
|
fn place(frame: &mut Frame, shaped: Shaped, width: Length, top: Length, bottom: Length) {
|
||||||
|
if !shaped.text.is_empty() {
|
||||||
|
frame.push(Point::new(frame.size.width, top), Element::Text(shaped));
|
||||||
|
frame.size.width += width;
|
||||||
|
frame.size.height = frame.size.height.max(top - bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Look up the glyph for `c` and returns its index alongside its advance width.
|
/// Look up the glyph for `c` and returns its index alongside its advance width.
|
||||||
fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> {
|
fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> {
|
||||||
let glyph = face.glyph_index(c)?;
|
let glyph = face.glyph_index(c)?;
|
||||||
@ -124,18 +167,32 @@ fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> {
|
|||||||
Some((glyph, width))
|
Some((glyph, width))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Place shaped text into a frame.
|
/// Look up a vertical metric.
|
||||||
fn place(
|
fn lookup_metric(face: &Face, metric: VerticalFontMetric) -> i16 {
|
||||||
frame: &mut Frame,
|
match metric {
|
||||||
shaped: Shaped,
|
VerticalFontMetric::Ascender => lookup_ascender(face),
|
||||||
offset: Length,
|
VerticalFontMetric::CapHeight => face
|
||||||
ascender: Length,
|
.capital_height()
|
||||||
descender: Length,
|
.filter(|&h| h > 0)
|
||||||
) {
|
.unwrap_or_else(|| lookup_ascender(face)),
|
||||||
if !shaped.text.is_empty() {
|
VerticalFontMetric::XHeight => face
|
||||||
let pos = Point::new(frame.size.width, ascender);
|
.x_height()
|
||||||
frame.push(pos, Element::Text(shaped));
|
.filter(|&h| h > 0)
|
||||||
frame.size.width += offset;
|
.unwrap_or_else(|| lookup_ascender(face)),
|
||||||
frame.size.height = frame.size.height.max(ascender - descender);
|
VerticalFontMetric::Baseline => 0,
|
||||||
|
VerticalFontMetric::Descender => lookup_descender(face),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The ascender of the face.
|
||||||
|
fn lookup_ascender(face: &Face) -> i16 {
|
||||||
|
// We prefer the typographic ascender over the Windows ascender because
|
||||||
|
// it can be overly large if the font has large glyphs.
|
||||||
|
face.typographic_ascender().unwrap_or_else(|| face.ascender())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The descender of the face.
|
||||||
|
fn lookup_descender(face: &Face) -> i16 {
|
||||||
|
// See `lookup_ascender` for reason.
|
||||||
|
face.typographic_descender().unwrap_or_else(|| face.descender())
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 706 B |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 929 B After Width: | Height: | Size: 880 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 512 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 827 B After Width: | Height: | Size: 776 B |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 840 B After Width: | Height: | Size: 803 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 36 KiB |
@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
// Syntax sugar for function definitions.
|
// Syntax sugar for function definitions.
|
||||||
#let background = #239dad
|
#let background = #9feb52
|
||||||
#let rect(body) = rect(width: 2cm, height: 1cm, fill: background, body)
|
#let rect(body) = rect(width: 2cm, fill: background, pad(5pt, body))
|
||||||
#rect[Hi!]
|
#rect[Hi!]
|
||||||
|
|
||||||
// Error: 13 expected body
|
// Error: 13 expected body
|
||||||
|
@ -30,8 +30,8 @@
|
|||||||
// the parentheses.
|
// the parentheses.
|
||||||
#align(center)[
|
#align(center)[
|
||||||
// Markdown-like syntax for headings.
|
// Markdown-like syntax for headings.
|
||||||
==== 3. Übungsblatt Computerorientierte Mathematik II #v(2mm)
|
==== 3. Übungsblatt Computerorientierte Mathematik II #v(4mm)
|
||||||
*Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(2mm)
|
*Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(4mm)
|
||||||
*Alle Antworten sind zu beweisen.*
|
*Alle Antworten sind zu beweisen.*
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -31,6 +31,20 @@ Emoji: 🐪, 🌋, 🏞
|
|||||||
∫ 𝛼 + 3𝛽 d𝑡
|
∫ 𝛼 + 3𝛽 d𝑡
|
||||||
]
|
]
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test top and bottom edge.
|
||||||
|
|
||||||
|
#page(width: 170pt)
|
||||||
|
#let try(top, bottom) = rect(fill: #9feb52)[
|
||||||
|
#font(top-edge: top, bottom-edge: bottom)
|
||||||
|
`From `#top` to `#bottom
|
||||||
|
]
|
||||||
|
|
||||||
|
#try(ascender, descender)
|
||||||
|
#try(ascender, baseline)
|
||||||
|
#try(cap-height, baseline)
|
||||||
|
#try(x-height, baseline)
|
||||||
|
|
||||||
---
|
---
|
||||||
// Ref: false
|
// Ref: false
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
// Ends paragraphs.
|
// Ends paragraphs.
|
||||||
Tightly #v(-5pt) packed
|
Tightly #v(0pt) packed
|
||||||
|
|
||||||
// Eating up soft spacing.
|
// Eating up soft spacing.
|
||||||
Inv #h(0pt) isible
|
Inv #h(0pt) isible
|
||||||
|
@ -28,3 +28,7 @@
|
|||||||
// Unterminated.
|
// Unterminated.
|
||||||
// Error: 6 expected closing brace
|
// Error: 6 expected closing brace
|
||||||
\u{41*Bold*
|
\u{41*Bold*
|
||||||
|
|
||||||
|
---
|
||||||
|
// Some code stuff in text.
|
||||||
|
let f() , ; : | + - /= == 12 "string"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
---
|
---
|
||||||
// Typst syntax inside.
|
// Typst syntax inside.
|
||||||
`#let x = 1` \
|
`#let x = 1` \
|
||||||
`#[f 1]`
|
`#f(1)`
|
||||||
|
|
||||||
---
|
---
|
||||||
// Multiline block splits paragraphs.
|
// Multiline block splits paragraphs.
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
// Test simple text.
|
// Test simple text.
|
||||||
|
|
||||||
---
|
#page(width: 250pt)
|
||||||
Hello 🌏!
|
|
||||||
|
|
||||||
---
|
But, soft! what light through yonder window breaks? It is the east, and Juliet
|
||||||
// Some code stuff in text.
|
is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and
|
||||||
let f() , ; : | + - /= == 12 "string"
|
pale with grief, That thou her maid art far more fair than she: Be not her maid,
|
||||||
|
since she is envious; Her vestal livery is but sick and green And none but fools
|
||||||
|
do wear it; cast it off. It is my lady, O, it is my love! O, that she knew she
|
||||||
|
were! She speaks yet she says nothing: what of that? Her eye discourses; I will
|
||||||
|
answer it.
|
||||||
|
|
||||||
|
I am too bold, 'tis not to me she speaks: Two of the fairest stars in all the
|
||||||
|
heaven, Having some business, do entreat her eyes To twinkle in their spheres
|
||||||
|
till they return. What if her eyes were there, they in her head? The brightness
|
||||||
|
of her cheek would shame those stars, As daylight doth a lamp; her eyes in
|
||||||
|
heaven Would through the airy region stream so bright That birds would sing and
|
||||||
|
think it were not night. See, how she leans her cheek upon her hand! O, that I
|
||||||
|
were a glove upon that hand, That I might touch that cheek!
|
||||||
|