use std::cell::RefCell;
use std::rc::Rc;
use parley::FontFamily;
use parley::FontWeight;
use smudge_core::element::Element;
use smudge_core::element::Value;
use smudge_core::geometry::Color;
use smudge_core::geometry::Point;
use smudge_core::geometry::Size;
use smudge_core::paint::GlyphRun;
use smudge_core::paint::PaintOp;
use smudge_core::semantics::{Role, Semantics};
use smudge_core::tree::Dirty;
use smudge_core::widget::AvailableSpace;
use smudge_core::widget::MeasureInput;
use smudge_core::widget::PaintCx;
use smudge_core::widget::Widget;
use crate::layout::TextLayout;
use crate::style::TextStyle;
use crate::style::current_text_style;
pub struct Label {
text: String,
style: TextStyle,
explicit: ExplicitStyle,
max_lines: Option<u32>,
layout: Option<TextLayout>,
broken_width: Option<Option<f32>>,
min_content: Option<Size>,
runs: RefCell<Option<CachedRuns>>,
}
struct CachedRuns {
origin: Point,
color: Color,
runs: Vec<Rc<GlyphRun>>,
}
impl Label {
fn new(text: String) -> Label {
Label {
text,
style: TextStyle::default(),
explicit: ExplicitStyle::default(),
max_lines: None,
layout: None,
broken_width: None,
min_content: None,
runs: RefCell::new(None),
}
}
fn invalidate_layout(&mut self) {
self.layout = None;
self.broken_width = None;
self.min_content = None;
*self.runs.borrow_mut() = None;
}
}
impl Widget for Label {
fn semantics(&self) -> Option<Semantics> {
Some(Semantics {
role: Role::Label,
label: None,
value: Some(self.text.clone()),
})
}
fn measure(&mut self, input: MeasureInput) -> Option<Size> {
if input.available_width == AvailableSpace::MinContent && input.known_width.is_none() {
if self.min_content.is_none() {
let painted = self.broken_width;
self.shape_and_break(Some(0.0));
let layout = self
.layout
.as_mut()
.expect("the layout was just built or is cached");
if matches!(self.max_lines, Some(0 | 1)) {
layout.truncate(self.max_lines, Some(0.0), &self.style);
}
self.min_content = Some(layout.size());
self.shape_and_show(painted.unwrap_or(None));
}
return self.min_content;
}
let max_width = wrap_width(input.available_width);
if self.broken_width != Some(max_width) {
self.shape_and_show(max_width);
}
let layout = self
.layout
.as_ref()
.expect("the layout was just built or is cached");
Some(layout.size())
}
fn paint(&self, cx: &PaintCx<'_>, out: &mut Vec<PaintOp>) {
let Some(layout) = &self.layout else {
return;
};
let color = self.text_color(cx);
let origin = cx.rect.origin();
let mut cache = self.runs.borrow_mut();
let rebuild = match &*cache {
Some(cached) => cached.origin != origin || cached.color != color,
None => true,
};
if rebuild {
let runs = layout
.glyph_runs_at(origin, color)
.into_iter()
.map(Rc::new)
.collect();
*cache = Some(CachedRuns {
origin,
color,
runs,
});
}
if let Some(cached) = cache.as_ref() {
out.extend(cached.runs.iter().cloned().map(PaintOp::Glyphs));
}
}
}
impl Label {
fn shape_and_show(&mut self, max_width: Option<f32>) {
self.shape_and_break(max_width);
if let Some(layout) = &mut self.layout {
layout.truncate(self.max_lines, max_width, &self.style);
}
}
fn shape_and_break(&mut self, max_width: Option<f32>) {
match &mut self.layout {
Some(layout) => layout.rebreak(max_width),
None => {
self.layout = Some(TextLayout::new(&self.text, &self.style, max_width));
}
}
self.broken_width = Some(max_width);
*self.runs.borrow_mut() = None;
}
fn text_color(&self, cx: &PaintCx<'_>) -> Color {
match &cx.style.paint.text_color {
Some(color) => color.resolve(cx.theme),
None => self.style.color.resolve(cx.theme),
}
}
}
pub fn label(text: impl Into<Value<String>>) -> Element<Label> {
Element::new(Label::new(String::new()))
.bind(AmbientStyleKey, current_text_style, apply_ambient_style)
.bind(TextKey, text, set_text)
}
pub trait LabelExt: Sized {
fn text_size(self, size: impl Into<Value<f32>>) -> Self;
fn weight(self, weight: FontWeight) -> Self;
fn family(self, family: FontFamily<'static>) -> Self;
fn max_lines(self, max_lines: impl Into<Value<Option<u32>>>) -> Self;
}
struct AmbientStyleKey;
struct TextKey;
struct TextSizeKey;
struct WeightKey;
struct FamilyKey;
struct MaxLinesKey;
impl LabelExt for Element<Label> {
fn text_size(self, size: impl Into<Value<f32>>) -> Self {
self.bind(TextSizeKey, size, set_text_size)
}
fn weight(self, weight: FontWeight) -> Self {
self.bind(WeightKey, Value::Static(weight), set_weight)
}
fn family(self, family: FontFamily<'static>) -> Self {
self.bind(FamilyKey, Value::Static(family), set_family)
}
fn max_lines(self, max_lines: impl Into<Value<Option<u32>>>) -> Self {
self.bind(MaxLinesKey, max_lines, set_max_lines)
}
}
#[derive(Clone, Copy, Default)]
struct ExplicitStyle {
family: bool,
size: bool,
weight: bool,
}
fn set_text(label: &mut Label, text: String) -> Dirty {
if label.text == text {
return Dirty::Clean;
}
label.text = text;
label.invalidate_layout();
Dirty::Layout
}
fn set_text_size(label: &mut Label, size: f32) -> Dirty {
label.explicit.size = true;
if label.style.size == size {
return Dirty::Clean;
}
label.style.size = size;
label.invalidate_layout();
Dirty::Layout
}
fn set_weight(label: &mut Label, weight: FontWeight) -> Dirty {
label.explicit.weight = true;
if label.style.weight == weight {
return Dirty::Clean;
}
label.style.weight = weight;
label.invalidate_layout();
Dirty::Layout
}
fn set_family(label: &mut Label, family: FontFamily<'static>) -> Dirty {
label.explicit.family = true;
if label.style.family == family {
return Dirty::Clean;
}
label.style.family = family;
label.invalidate_layout();
Dirty::Layout
}
fn set_max_lines(label: &mut Label, max_lines: Option<u32>) -> Dirty {
if label.max_lines == max_lines {
return Dirty::Clean;
}
label.max_lines = max_lines;
label.broken_width = None;
label.min_content = None;
*label.runs.borrow_mut() = None;
Dirty::Layout
}
fn apply_ambient_style(label: &mut Label, mut ambient: TextStyle) -> Dirty {
let explicit = label.explicit;
if explicit.family {
ambient.family = label.style.family.clone();
}
if explicit.size {
ambient.size = label.style.size;
}
if explicit.weight {
ambient.weight = label.style.weight;
}
if ambient == label.style {
return Dirty::Clean;
}
label.style = ambient;
label.invalidate_layout();
Dirty::Layout
}
fn wrap_width(space: AvailableSpace) -> Option<f32> {
match space {
AvailableSpace::Definite(width) => Some(width),
AvailableSpace::MaxContent => None,
AvailableSpace::MinContent => Some(0.0),
}
}
#[cfg(test)]
#[path = "label_tests.rs"]
mod tests;