use std::rc::Rc;
use crate::model::{self, ParagraphProperties};
use crate::render::dimension::Pt;
use crate::render::layout::fragment::Fragment;
use super::convert::{pic_bullet_size, remap_legacy_font_chars, resolve_paragraph_defaults};
use super::{BuildContext, BuildState};
pub(super) fn inject_list_label(
para: &model::Paragraph,
fragments: &mut Vec<Fragment>,
merged_props: &mut ParagraphProperties,
ctx: &BuildContext,
state: &mut BuildState,
) {
let num_ref = match merged_props.numbering {
Some(ref nr) => nr,
None => return,
};
let num_id = model::NumId::new(num_ref.num_id);
let level = num_ref.level;
let levels = match ctx.resolved.numbering.get(&num_id) {
Some(levels) => levels,
None => return,
};
{
let counters = &mut state.list_counters;
let count = counters
.entry((num_id, level))
.or_insert_with(|| levels.get(level as usize).map(|l| l.start).unwrap_or(1) - 1);
*count += 1;
let max_level = levels.len() as u8;
for deeper in (level + 1)..max_level {
counters.remove(&(num_id, deeper));
}
}
let level_def = levels.get(level as usize);
let pic_bullet_injected = level_def
.and_then(|l| l.lvl_pic_bullet_id)
.and_then(|pic_id| ctx.resolved.pic_bullets.get(&pic_id))
.and_then(|bullet| {
let rel_id = bullet
.pict
.as_ref()?
.shapes()
.next()?
.common
.image_data
.as_ref()?
.rel_id
.as_ref()?;
let image_bytes = ctx.media().get(rel_id)?;
let size = pic_bullet_size(bullet);
let label_frag = Fragment::Image {
size,
rel_id: rel_id.as_str().to_string(),
image_data: Some(image_bytes.clone()),
};
Some((label_frag, size.height))
});
if let Some((label_frag, label_height)) = pic_bullet_injected {
let hanging = extract_hanging(level_def);
let tab_frag = Fragment::Tab {
line_height: label_height,
fitting_width: Some(hanging),
};
fragments.insert(0, tab_frag);
fragments.insert(0, label_frag);
if let Some(lvl_left) = level_def
.and_then(|l| l.indentation.as_ref())
.and_then(|ind| ind.start)
{
merged_props.tabs.insert(
0,
crate::model::TabStop {
position: lvl_left,
alignment: crate::model::TabAlignment::Left,
leader: crate::model::TabLeader::None,
},
);
}
} else {
inject_text_label(
para,
fragments,
merged_props,
ctx,
&state.list_counters,
levels,
level,
level_def,
);
}
if let Some(lvl_ind) = levels
.get(level as usize)
.and_then(|l| l.indentation.as_ref())
{
let mut ind = *lvl_ind;
if let Some(direct) = para.properties.indentation {
if let Some(start) = direct.start {
ind.start = Some(start);
}
if let Some(end) = direct.end {
ind.end = Some(end);
}
if let Some(first_line) = direct.first_line {
ind.first_line = Some(first_line);
}
}
merged_props.indentation = Some(ind);
}
}
#[allow(clippy::too_many_arguments)]
fn inject_text_label(
para: &model::Paragraph,
fragments: &mut Vec<Fragment>,
merged_props: &mut ParagraphProperties,
ctx: &BuildContext,
counters: &std::collections::HashMap<(model::NumId, u8), u32>,
levels: &[crate::render::resolve::numbering::ResolvedNumberingLevel],
level: u8,
level_def: Option<&crate::render::resolve::numbering::ResolvedNumberingLevel>,
) {
let num_id = model::NumId::new(merged_props.numbering.as_ref().unwrap().num_id);
let label_text =
match crate::render::resolve::numbering::format_list_label(levels, level, counters, num_id)
{
Some(t) => t,
None => return,
};
let (default_family, default_size, default_color, _, paragraph_style_run) =
resolve_paragraph_defaults(para, ctx.resolved, false);
let cascade = ListLabelRunPropertyCascade {
level: level_def.and_then(|l| l.run_properties.as_ref()),
paragraph_mark: para.mark_run_properties.as_ref(),
paragraph_style: Some(¶graph_style_run),
};
let label_color = cascade
.pick(|rp| rp.color)
.map(|c| {
crate::render::resolve::color::resolve_color(
c,
crate::render::resolve::color::ColorContext::Text,
)
})
.unwrap_or(default_color);
let cascade_family = cascade
.iter()
.find_map(|rp| crate::render::resolve::fonts::effective_font(&rp.fonts))
.unwrap_or("");
let (label_text, label_family) =
remap_legacy_font_chars(&label_text, cascade_family, &default_family);
let mut label_font = build_label_font_props(&cascade, &default_family, default_size);
if label_family != *label_font.family {
label_font.family = Rc::from(label_family.as_str());
}
populate_label_underline_metrics(&mut label_font, ctx.measurer);
let (w, m) = ctx.measurer.measure(&label_text, &label_font);
let h = m.height();
let hanging = extract_hanging(level_def);
let jc = level_def.and_then(|l| l.justification);
let text_offset = match jc {
Some(crate::model::Alignment::End) => -w,
Some(crate::model::Alignment::Center) => w * -0.5,
_ => Pt::ZERO,
};
let label_width = w;
let label_frag = Fragment::Text {
text: Rc::from(label_text.as_str()),
font: label_font.clone(),
color: label_color,
shading: None,
border: None,
width: label_width,
trimmed_width: label_width,
metrics: m,
hyperlink_url: None,
baseline_offset: Pt::ZERO,
text_offset,
};
let tab_fitting = (hanging - label_width).max(Pt::ZERO);
let tab_frag = Fragment::Tab {
line_height: h,
fitting_width: Some(tab_fitting),
};
fragments.insert(0, tab_frag);
fragments.insert(0, label_frag);
let lvl_left = level_def
.and_then(|l| l.indentation.as_ref())
.and_then(|ind| ind.start);
if let Some(lvl_left) = lvl_left {
merged_props.tabs.insert(
0,
crate::model::TabStop {
position: lvl_left,
alignment: crate::model::TabAlignment::Left,
leader: crate::model::TabLeader::None,
},
);
}
}
fn extract_hanging(
level_def: Option<&crate::render::resolve::numbering::ResolvedNumberingLevel>,
) -> Pt {
level_def
.and_then(|l| l.indentation.as_ref())
.and_then(|ind| ind.first_line)
.map(|fl| match fl {
model::FirstLineIndent::Hanging(v) => Pt::from(v),
_ => Pt::ZERO,
})
.unwrap_or(Pt::ZERO)
}
pub(super) fn build_label_font_props(
cascade: &ListLabelRunPropertyCascade<'_>,
default_family: &str,
default_size: Pt,
) -> crate::render::layout::fragment::FontProps {
let effective = cascade.resolve();
crate::render::layout::fragment::font_props_from_run(&effective, default_family, default_size)
}
pub(super) fn populate_label_underline_metrics(
font: &mut crate::render::layout::fragment::FontProps,
measurer: &crate::render::layout::measurer::TextMeasurer,
) {
if font.underline {
let (pos, thickness) = measurer.underline_metrics(font);
font.underline_position = pos;
font.underline_thickness = thickness;
}
}
pub(super) struct ListLabelRunPropertyCascade<'a> {
pub level: Option<&'a model::RunProperties>,
pub paragraph_mark: Option<&'a model::RunProperties>,
pub paragraph_style: Option<&'a model::RunProperties>,
}
impl<'a> ListLabelRunPropertyCascade<'a> {
pub(super) fn iter(&self) -> impl Iterator<Item = &'a model::RunProperties> + '_ {
[self.level, self.paragraph_mark, self.paragraph_style]
.into_iter()
.flatten()
}
pub(super) fn pick<T: Copy>(
&self,
get: impl Fn(&model::RunProperties) -> Option<T>,
) -> Option<T> {
self.iter().find_map(get)
}
pub(super) fn resolve(&self) -> model::RunProperties {
let fonts = self
.iter()
.find(|rp| crate::render::resolve::fonts::effective_font(&rp.fonts).is_some())
.map(|rp| rp.fonts.clone())
.unwrap_or_default();
model::RunProperties {
fonts,
font_size: self.pick(|rp| rp.font_size),
bold: self.pick(|rp| rp.bold),
italic: self.pick(|rp| rp.italic),
underline: self.pick(|rp| rp.underline),
color: self.pick(|rp| rp.color),
spacing: self.pick(|rp| rp.spacing),
text_scale: self.pick(|rp| rp.text_scale),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::dimension::{Dimension, HalfPoints, Twips};
use crate::model::{FontSet, FontSlot, RunProperties, TextScale, UnderlineStyle};
fn rp_with_bold(b: bool) -> RunProperties {
RunProperties {
bold: Some(b),
..Default::default()
}
}
fn rp_with_underline(u: UnderlineStyle) -> RunProperties {
RunProperties {
underline: Some(u),
..Default::default()
}
}
fn rp_with_font(name: &str) -> RunProperties {
RunProperties {
fonts: FontSet {
ascii: FontSlot::from_name(name),
..Default::default()
},
..Default::default()
}
}
#[test]
fn cascade_pick_level_overrides_mark_for_bold() {
let level = rp_with_bold(true);
let mark = rp_with_bold(false);
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: Some(&mark),
paragraph_style: None,
};
assert_eq!(cascade.pick(|rp| rp.bold), Some(true));
}
#[test]
fn cascade_pick_falls_through_to_mark_when_level_field_absent() {
let level = rp_with_bold(true); let mark = rp_with_underline(UnderlineStyle::Single);
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: Some(&mark),
paragraph_style: None,
};
assert_eq!(cascade.pick(|rp| rp.bold), Some(true));
assert_eq!(
cascade.pick(|rp| rp.underline),
Some(UnderlineStyle::Single)
);
}
#[test]
fn cascade_pick_falls_through_to_paragraph_style_when_others_absent() {
let style = rp_with_underline(UnderlineStyle::Double);
let cascade = ListLabelRunPropertyCascade {
level: None,
paragraph_mark: None,
paragraph_style: Some(&style),
};
assert_eq!(
cascade.pick(|rp| rp.underline),
Some(UnderlineStyle::Double)
);
}
#[test]
fn cascade_pick_returns_none_when_no_source_sets_field() {
let level = rp_with_bold(true);
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: None,
paragraph_style: None,
};
assert_eq!(cascade.pick(|rp| rp.italic), None);
}
#[test]
fn cascade_iter_skips_none_layers_in_order() {
let level = rp_with_bold(true);
let style = rp_with_bold(false);
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: None, paragraph_style: Some(&style),
};
let bolds: Vec<_> = cascade.iter().map(|rp| rp.bold).collect();
assert_eq!(bolds, vec![Some(true), Some(false)]);
}
#[test]
fn cascade_resolve_materializes_underline_from_level() {
let level = rp_with_underline(UnderlineStyle::Single);
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: None,
paragraph_style: None,
};
let effective = cascade.resolve();
assert_eq!(effective.underline, Some(UnderlineStyle::Single));
}
#[test]
fn cascade_resolve_fonts_picks_first_explicit_source() {
let level = RunProperties::default(); let mark = rp_with_font("Verdana");
let style = rp_with_font("Arial");
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: Some(&mark),
paragraph_style: Some(&style),
};
let effective = cascade.resolve();
assert_eq!(effective.fonts.ascii.explicit.as_deref(), Some("Verdana"));
}
#[test]
fn label_font_inherits_underline_from_level() {
let level = rp_with_underline(UnderlineStyle::Single);
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: None,
paragraph_style: None,
};
let font = build_label_font_props(&cascade, "Helvetica", Pt::new(12.0));
assert!(
font.underline,
"level rPr <w:u/> must become font.underline"
);
}
#[test]
fn label_font_inherits_underline_from_mark_when_level_absent() {
let mark = rp_with_underline(UnderlineStyle::Single);
let cascade = ListLabelRunPropertyCascade {
level: None,
paragraph_mark: Some(&mark),
paragraph_style: None,
};
let font = build_label_font_props(&cascade, "Helvetica", Pt::new(12.0));
assert!(font.underline);
}
#[test]
fn label_font_underline_false_when_level_explicitly_none() {
let level = rp_with_underline(UnderlineStyle::None);
let mark = rp_with_underline(UnderlineStyle::Single);
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: Some(&mark),
paragraph_style: None,
};
let font = build_label_font_props(&cascade, "Helvetica", Pt::new(12.0));
assert!(
!font.underline,
"explicit UnderlineStyle::None must override lower layers"
);
}
#[test]
fn label_font_inherits_char_spacing_from_level() {
let level = RunProperties {
spacing: Some(Dimension::<Twips>::new(40)),
..Default::default()
};
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: None,
paragraph_style: None,
};
let font = build_label_font_props(&cascade, "Helvetica", Pt::new(12.0));
assert!((font.char_spacing.raw() - 2.0).abs() < 1e-4);
}
#[test]
fn label_font_inherits_text_scale_from_level() {
let level = RunProperties {
text_scale: Some(TextScale::new(150)),
..Default::default()
};
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: None,
paragraph_style: None,
};
let font = build_label_font_props(&cascade, "Helvetica", Pt::new(12.0));
assert!((font.text_scale - 1.5).abs() < 1e-4);
}
#[test]
fn label_font_pass_through_basic_fields() {
let level = RunProperties {
bold: Some(true),
italic: Some(true),
font_size: Some(Dimension::<HalfPoints>::new(24)), fonts: FontSet {
ascii: FontSlot::from_name("Verdana"),
..Default::default()
},
..Default::default()
};
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: None,
paragraph_style: None,
};
let font = build_label_font_props(&cascade, "Helvetica", Pt::new(10.0));
assert!(font.bold);
assert!(font.italic);
assert_eq!(font.size.raw(), 12.0);
assert_eq!(&*font.family, "Verdana");
}
#[test]
fn label_font_falls_back_to_default_family() {
let cascade = ListLabelRunPropertyCascade {
level: None,
paragraph_mark: None,
paragraph_style: None,
};
let font = build_label_font_props(&cascade, "Helvetica", Pt::new(12.0));
assert_eq!(&*font.family, "Helvetica");
}
#[test]
fn cascade_resolve_returns_defaults_when_cascade_empty() {
let cascade = ListLabelRunPropertyCascade {
level: None,
paragraph_mark: None,
paragraph_style: None,
};
let effective = cascade.resolve();
assert_eq!(effective, RunProperties::default());
}
#[test]
fn cascade_resolve_composes_all_label_fields() {
use crate::model::Color;
let level = RunProperties {
bold: Some(true),
underline: Some(UnderlineStyle::Single),
..Default::default()
};
let mark = RunProperties {
italic: Some(true),
font_size: Some(Dimension::<HalfPoints>::new(24)),
spacing: Some(Dimension::<Twips>::new(40)),
..Default::default()
};
let style = RunProperties {
color: Some(Color::Rgb(0x112233)),
text_scale: Some(TextScale::new(120)),
fonts: FontSet {
ascii: FontSlot::from_name("Calibri"),
..Default::default()
},
..Default::default()
};
let cascade = ListLabelRunPropertyCascade {
level: Some(&level),
paragraph_mark: Some(&mark),
paragraph_style: Some(&style),
};
let effective = cascade.resolve();
assert_eq!(effective.bold, Some(true), "from level");
assert_eq!(
effective.underline,
Some(UnderlineStyle::Single),
"from level"
);
assert_eq!(effective.italic, Some(true), "from mark");
assert_eq!(
effective.font_size,
Some(Dimension::<HalfPoints>::new(24)),
"from mark"
);
assert_eq!(
effective.spacing,
Some(Dimension::<Twips>::new(40)),
"from mark"
);
assert_eq!(effective.color, Some(Color::Rgb(0x112233)), "from style");
assert_eq!(
effective.text_scale,
Some(TextScale::new(120)),
"from style"
);
assert_eq!(
effective.fonts.ascii.explicit.as_deref(),
Some("Calibri"),
"from style (lowest source that sets fonts)"
);
}
}