use std::any::TypeId;
use std::mem::Discriminant;
use accesskit::{Node, NodeId, Role};
use masonry_core::core::{HasProperty, NoAction};
use parley::{Layout, LayoutAccessibility};
use tracing::{Span, trace_span};
use vello::Scene;
use vello::kurbo::{Affine, Point, Size};
use vello::peniko::BlendMode;
use crate::core::{
AccessCtx, ArcStr, BoxConstraints, BrushIndex, ChildrenIds, LayoutCtx, PaintCtx, PropertiesMut,
PropertiesRef, RegisterCtx, StyleProperty, StyleSet, Update, UpdateCtx, Widget, WidgetId,
WidgetMut, render_text,
};
use crate::properties::{ContentColor, DisabledContentColor, LineBreaking, Padding};
use crate::theme::default_text_styles;
use crate::util::{debug_panic, include_screenshot};
use crate::{TextAlign, TextAlignOptions, theme};
#[doc = include_screenshot!("label_styled_label.png", "Styled label.")]
pub struct Label {
text_layout: Layout<BrushIndex>,
accessibility: LayoutAccessibility,
text: ArcStr,
styles: StyleSet,
styles_changed: bool,
text_alignment: TextAlign,
needs_text_alignment: bool,
last_available_width: Option<f32>,
last_max_advance: Option<f32>,
hint: bool,
}
impl Label {
pub fn new(text: impl Into<ArcStr>) -> Self {
let mut styles = StyleSet::new(theme::TEXT_SIZE_NORMAL);
default_text_styles(&mut styles);
Self {
text_layout: Layout::new(),
accessibility: LayoutAccessibility::default(),
text: text.into(),
styles,
styles_changed: true,
text_alignment: TextAlign::Start,
needs_text_alignment: true,
last_available_width: None,
last_max_advance: None,
hint: true,
}
}
pub fn text(&self) -> &ArcStr {
&self.text
}
pub fn with_style(mut self, property: impl Into<StyleProperty>) -> Self {
self.insert_style_inner(property.into());
self
}
pub fn try_with_style(
mut self,
property: impl Into<StyleProperty>,
) -> (Self, Option<StyleProperty>) {
let old = self.insert_style_inner(property.into());
(self, old)
}
pub fn with_text_alignment(mut self, text_alignment: TextAlign) -> Self {
self.text_alignment = text_alignment;
self
}
pub fn with_hint(mut self, hint: bool) -> Self {
self.hint = hint;
self
}
fn insert_style_inner(&mut self, property: StyleProperty) -> Option<StyleProperty> {
if let StyleProperty::Brush(idx @ BrushIndex(1..))
| StyleProperty::UnderlineBrush(Some(idx @ BrushIndex(1..)))
| StyleProperty::StrikethroughBrush(Some(idx @ BrushIndex(1..))) = &property
{
debug_panic!(
"Can't set a non-zero brush index ({idx:?}) on a `Label`, as it only supports global styling."
);
}
self.styles.insert(property)
}
}
impl Label {
pub fn insert_style(
this: &mut WidgetMut<'_, Self>,
property: impl Into<StyleProperty>,
) -> Option<StyleProperty> {
let old = this.widget.insert_style_inner(property.into());
this.widget.styles_changed = true;
this.ctx.request_layout();
old
}
pub fn retain_styles(this: &mut WidgetMut<'_, Self>, f: impl FnMut(&StyleProperty) -> bool) {
this.widget.styles.retain(f);
this.widget.styles_changed = true;
this.ctx.request_layout();
}
pub fn remove_style(
this: &mut WidgetMut<'_, Self>,
property: Discriminant<StyleProperty>,
) -> Option<StyleProperty> {
let old = this.widget.styles.remove(property);
this.widget.styles_changed = true;
this.ctx.request_layout();
old
}
pub fn set_text(this: &mut WidgetMut<'_, Self>, new_text: impl Into<ArcStr>) {
this.widget.text = new_text.into();
this.widget.styles_changed = true;
this.ctx.request_layout();
}
pub fn set_text_alignment(this: &mut WidgetMut<'_, Self>, text_alignment: TextAlign) {
this.widget.text_alignment = text_alignment;
this.widget.needs_text_alignment = true;
this.ctx.request_layout();
}
pub fn set_hint(this: &mut WidgetMut<'_, Self>, hint: bool) {
this.widget.hint = hint;
this.ctx.request_paint_only();
}
}
impl HasProperty<ContentColor> for Label {}
impl HasProperty<DisabledContentColor> for Label {}
impl HasProperty<LineBreaking> for Label {}
impl Widget for Label {
type Action = NoAction;
fn accepts_pointer_interaction(&self) -> bool {
false
}
fn register_children(&mut self, _ctx: &mut RegisterCtx<'_>) {}
fn property_changed(&mut self, ctx: &mut UpdateCtx<'_>, property_type: TypeId) {
LineBreaking::prop_changed(ctx, property_type);
ContentColor::prop_changed(ctx, property_type);
DisabledContentColor::prop_changed(ctx, property_type);
Padding::prop_changed(ctx, property_type);
}
fn update(&mut self, ctx: &mut UpdateCtx<'_>, _props: &mut PropertiesMut<'_>, event: &Update) {
match event {
Update::DisabledChanged(_) => {
ctx.request_paint_only();
}
_ => {}
}
}
fn layout(
&mut self,
ctx: &mut LayoutCtx<'_>,
props: &mut PropertiesMut<'_>,
bc: &BoxConstraints,
) -> Size {
let padding = *props.get::<Padding>();
let line_break_mode = *props.get::<LineBreaking>();
let bc = padding.layout_down(*bc);
let available_width = Some(bc.max().width as f32);
if available_width != self.last_available_width {
self.last_available_width = available_width;
self.needs_text_alignment = true;
}
let max_advance = if line_break_mode == LineBreaking::WordWrap {
available_width
} else {
None
};
let styles_changed = self.styles_changed || ctx.fonts_changed();
if styles_changed {
let (font_ctx, layout_ctx) = ctx.text_contexts();
let mut builder = layout_ctx.ranged_builder(font_ctx, &self.text, 1.0, true);
for prop in self.styles.inner().values() {
builder.push_default(prop.to_owned());
}
builder.build_into(&mut self.text_layout, &self.text);
self.styles_changed = false;
}
if max_advance != self.last_max_advance || styles_changed {
self.text_layout.break_all_lines(max_advance);
self.last_max_advance = max_advance;
self.needs_text_alignment = true;
}
let alignment_width = if self.text_alignment == TextAlign::Start {
self.text_layout.width()
} else if let Some(width) = available_width {
width
} else {
self.text_layout.width()
};
if self.needs_text_alignment {
self.text_layout.align(
Some(alignment_width),
self.text_alignment,
TextAlignOptions::default(),
);
self.needs_text_alignment = false;
}
let size = Size::new(alignment_width.into(), self.text_layout.height().into());
let size = bc.constrain(size);
let (size, baseline) = padding.layout_up(size, 0.);
ctx.set_baseline_offset(baseline);
size
}
fn paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) {
let padding = *props.get::<Padding>();
let line_break_mode = *props.get::<LineBreaking>();
if line_break_mode == LineBreaking::Clip {
let clip_rect = ctx.size().to_rect();
scene.push_layer(BlendMode::default(), 1., Affine::IDENTITY, &clip_rect);
}
let text_origin = padding.place_down(Point::ZERO).to_vec2();
let transform = Affine::translate(text_origin);
let text_color = if ctx.is_disabled() {
&props.get::<DisabledContentColor>().0
} else {
props.get::<ContentColor>()
};
render_text(
scene,
transform,
&self.text_layout,
&[text_color.color.into()],
self.hint,
);
if line_break_mode == LineBreaking::Clip {
scene.pop_layer();
}
}
fn accessibility_role(&self) -> Role {
Role::Label
}
fn accessibility(
&mut self,
ctx: &mut AccessCtx<'_>,
props: &PropertiesRef<'_>,
node: &mut Node,
) {
let padding = *props.get::<Padding>();
let text_origin = padding.place_down(Point::ZERO).to_vec2();
self.accessibility.build_nodes(
self.text.as_ref(),
&self.text_layout,
ctx.tree_update(),
node,
|| NodeId::from(WidgetId::next()),
text_origin.x,
text_origin.y,
);
}
fn children_ids(&self) -> ChildrenIds {
ChildrenIds::new()
}
fn make_trace_span(&self, id: WidgetId) -> Span {
trace_span!("Label", id = id.trace())
}
fn get_debug_text(&self) -> Option<String> {
Some(self.text.to_string())
}
}
#[cfg(test)]
mod tests {
use parley::style::GenericFamily;
use parley::{FontFamily, StyleProperty};
use super::*;
use crate::core::Properties;
use crate::properties::types::CrossAxisAlignment;
use crate::properties::types::{AsUnit, Length};
use crate::testing::{TestHarness, assert_render_snapshot};
use crate::theme::{ACCENT_COLOR, default_property_set};
use crate::widgets::{Flex, SizedBox};
#[test]
fn simple_label() {
let label = Label::new("Hello").with_auto_id();
let window_size = Size::new(100.0, 40.0);
let mut harness = TestHarness::create_with_size(default_property_set(), label, window_size);
assert_render_snapshot!(harness, "label_hello");
}
#[test]
fn styled_label() {
let label = Label::new("The quick brown fox jumps over the lazy dog")
.with_style(FontFamily::Generic(GenericFamily::Monospace))
.with_style(StyleProperty::FontSize(20.0))
.with_text_alignment(TextAlign::Center)
.with_props(
Properties::new()
.with(ContentColor::new(ACCENT_COLOR))
.with(LineBreaking::WordWrap),
);
let mut harness =
TestHarness::create_with_size(default_property_set(), label, Size::new(200.0, 200.0));
assert_render_snapshot!(harness, "label_styled_label");
}
#[test]
fn underline_label() {
let label = Label::new("Emphasis")
.with_style(StyleProperty::Underline(true))
.with_props(Properties::new().with(LineBreaking::WordWrap));
let window_size = Size::new(100.0, 40.0);
let mut harness = TestHarness::create_with_size(default_property_set(), label, window_size);
assert_render_snapshot!(harness, "label_underline_label");
}
#[test]
fn strikethrough_label() {
let label = Label::new("Tpyo")
.with_style(StyleProperty::Strikethrough(true))
.with_style(StyleProperty::StrikethroughSize(Some(4.)))
.with_props(Properties::new().with(LineBreaking::WordWrap));
let window_size = Size::new(100.0, 40.0);
let mut harness = TestHarness::create_with_size(default_property_set(), label, window_size);
assert_render_snapshot!(harness, "label_strikethrough_label");
}
#[test]
fn label_text_alignment_flex() {
fn base_label() -> Label {
Label::new("Hello").with_style(StyleProperty::FontSize(20.0))
}
let label1 = base_label().with_text_alignment(TextAlign::Start);
let label2 = base_label().with_text_alignment(TextAlign::Center);
let label3 = base_label().with_text_alignment(TextAlign::End);
let label4 = base_label().with_text_alignment(TextAlign::Start);
let label5 = base_label().with_text_alignment(TextAlign::Center);
let label6 = base_label().with_text_alignment(TextAlign::End);
let flex = Flex::column()
.with_flex_child(label1.with_auto_id(), CrossAxisAlignment::Start)
.with_flex_child(label2.with_auto_id(), CrossAxisAlignment::Start)
.with_flex_child(label3.with_auto_id(), CrossAxisAlignment::Start)
.with_flex_child(label4.with_auto_id(), CrossAxisAlignment::Center)
.with_flex_child(label5.with_auto_id(), CrossAxisAlignment::Center)
.with_flex_child(label6.with_auto_id(), CrossAxisAlignment::Center)
.with_gap(Length::ZERO)
.with_auto_id();
let mut harness =
TestHarness::create_with_size(default_property_set(), flex, Size::new(200.0, 200.0));
assert_render_snapshot!(harness, "label_label_alignment_flex");
}
#[test]
fn line_break_modes() {
let widget = Flex::column()
.with_flex_spacer(1.0)
.with_child(
SizedBox::new(
Label::new("The quick brown fox jumps over the lazy dog")
.with_props(Properties::new().with(LineBreaking::WordWrap)),
)
.width(180.px())
.with_auto_id(),
)
.with_spacer(20.px())
.with_child(
SizedBox::new(
Label::new("The quick brown fox jumps over the lazy dog")
.with_props(Properties::new().with(LineBreaking::Clip)),
)
.width(180.px())
.with_auto_id(),
)
.with_spacer(20.px())
.with_child(
SizedBox::new(
Label::new("The quick brown fox jumps over the lazy dog")
.with_props(Properties::new().with(LineBreaking::Overflow)),
)
.width(180.px())
.with_auto_id(),
)
.with_flex_spacer(1.0)
.with_auto_id();
let mut harness =
TestHarness::create_with_size(default_property_set(), widget, Size::new(200.0, 200.0));
assert_render_snapshot!(harness, "label_line_break_modes");
}
#[test]
fn edit_label() {
let image_1 = {
let label = Label::new("The quick brown fox jumps over the lazy dog")
.with_style(FontFamily::Generic(GenericFamily::Monospace))
.with_style(StyleProperty::FontSize(20.0))
.with_text_alignment(TextAlign::Center)
.with_props(
Properties::new()
.with(ContentColor::new(ACCENT_COLOR))
.with(LineBreaking::WordWrap),
);
let mut harness =
TestHarness::create_with_size(default_property_set(), label, Size::new(50.0, 50.0));
harness.render()
};
let image_2 = {
let label = Label::new("Hello world")
.with_style(StyleProperty::FontSize(40.0))
.with_auto_id();
let mut harness =
TestHarness::create_with_size(default_property_set(), label, Size::new(50.0, 50.0));
harness.edit_root_widget(|mut label| {
label.insert_prop(ContentColor::new(ACCENT_COLOR));
label.insert_prop(LineBreaking::WordWrap);
Label::set_text(&mut label, "The quick brown fox jumps over the lazy dog");
Label::insert_style(&mut label, FontFamily::Generic(GenericFamily::Monospace));
Label::insert_style(&mut label, StyleProperty::FontSize(20.0));
Label::set_text_alignment(&mut label, TextAlign::Center);
});
harness.render()
};
assert!(image_1 == image_2);
}
}