From 34e8bf763633c5fb922529c64c66e581f0049ed1 Mon Sep 17 00:00:00 2001 From: wjian23 Date: Wed, 22 Jan 2025 15:36:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=F0=9F=8E=B8=20Add=20builtin=20fi?= =?UTF-8?q?eld=20`clip=5Fboundary`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 + core/src/builtin_widgets.rs | 28 ++++++- core/src/builtin_widgets/border.rs | 28 +++---- core/src/builtin_widgets/clip.rs | 42 +++------- core/src/builtin_widgets/clip_boundary.rs | 77 ++++++++++++++++++ core/src/builtin_widgets/scrollable.rs | 8 +- core/src/builtin_widgets/text.rs | 27 +++--- core/src/builtin_widgets/theme.rs | 2 +- macros/src/variable_names.rs | 4 + painter/src/painter.rs | 3 - painter/src/text.rs | 2 +- painter/src/text/typography_store.rs | 22 ++--- .../clip_boundary/tests/clip_boundary.png | Bin 0 -> 663 bytes themes/material/src/lib.rs | 2 +- themes/ribir_slim/src/lib.rs | 2 +- widgets/src/avatar.rs | 3 +- widgets/src/lists.rs | 1 + 17 files changed, 175 insertions(+), 79 deletions(-) create mode 100644 core/src/builtin_widgets/clip_boundary.rs create mode 100644 test_cases/ribir_core/builtin_widgets/clip_boundary/tests/clip_boundary.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 698372574..61ce4c154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ## [@Unreleased] - @ReleaseDate +### Features +- **core**: Add builtin filed `clip_boundary`. (#694 @wjian23) + ## [0.4.0-alpha.24] - 2025-01-22 ### Features diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index f789fea59..dab669dc8 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -53,6 +53,8 @@ pub use svg::*; pub mod clip; pub use clip::*; +pub mod clip_boundary; +pub use clip_boundary::*; pub mod focus_node; pub use focus_node::*; pub mod focus_scope; @@ -146,6 +148,7 @@ pub struct FatObj { keep_alive: Option>, keep_alive_unsubscribe_handle: Option>, tooltips: Option>, + clip_boundary: Option>, providers: Option>, } @@ -194,6 +197,7 @@ impl FatObj { visibility: self.visibility, opacity: self.opacity, tooltips: self.tooltips, + clip_boundary: self.clip_boundary, keep_alive: self.keep_alive, keep_alive_unsubscribe_handle: self.keep_alive_unsubscribe_handle, providers: self.providers, @@ -228,6 +232,7 @@ impl FatObj { && self.opacity.is_none() && self.keep_alive.is_none() && self.tooltips.is_none() + && self.clip_boundary.is_none() } /// Return the host object of the FatObj. @@ -470,6 +475,14 @@ impl FatObj { .tooltips .get_or_insert_with(|| State::value(<_>::default())) } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_clip_boundary_widget(&mut self) -> &State { + self + .clip_boundary + .get_or_insert_with(|| State::value(<_>::default())) + } } macro_rules! on_mixin { @@ -931,6 +944,11 @@ impl FatObj { self.declare_builtin_init(v, Self::get_tooltips_widget, |m, v| m.tooltips = v) } + /// Initializes the clip_boundary of the widget. + pub fn clip_boundary(self, v: impl DeclareInto) -> Self { + self.declare_builtin_init(v, Self::get_clip_boundary_widget, |m, v| m.clip_boundary = v) + } + /// Initializes the `keep_alive` value of the `KeepAlive` widget. pub fn keep_alive(mut self, v: impl DeclareInto) -> Self { let (v, o) = v.declare_into().unzip(); @@ -1041,7 +1059,15 @@ impl<'a> FatObj> { compose_builtin_widgets!( host + [ - track_id, padding, fitted_box, foreground, border, background, radius, scrollable, + track_id, + padding, + fitted_box, + foreground, + border, + background, + clip_boundary, + radius, + scrollable, layout_box ] ); diff --git a/core/src/builtin_widgets/border.rs b/core/src/builtin_widgets/border.rs index a3f09c962..e82b02313 100644 --- a/core/src/builtin_widgets/border.rs +++ b/core/src/builtin_widgets/border.rs @@ -550,7 +550,7 @@ mod tests { @ { border_100_50_box(10., 0., 0., 0., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "top_borders" ); @@ -564,7 +564,7 @@ mod tests { @ { border_100_50_box(0., 10., 0., 0., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "right_borders" ); @@ -578,7 +578,7 @@ mod tests { @ { border_100_50_box(0., 0., 10., 0., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "bottom_borders" ); @@ -592,7 +592,7 @@ mod tests { @ { border_100_50_box(0., 0., 0., 10., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "left_borders" ); } @@ -612,7 +612,7 @@ mod tests { @ { border_100_50_box(10., 0., 10., 0., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "top_and_bottom_borders" ); @@ -626,7 +626,7 @@ mod tests { @ { border_100_50_box(0., 10., 0., 10., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "left_and_right_borders" ); @@ -640,7 +640,7 @@ mod tests { @ { border_100_50_box(10., 0., 0., 10., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "top_left_borders" ); @@ -654,7 +654,7 @@ mod tests { @ { border_100_50_box(10., 10., 0., 0., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "top_right_borders" ); @@ -668,7 +668,7 @@ mod tests { @ { border_100_50_box(0., 10., 10., 0., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "right_bottom_borders" ); @@ -682,7 +682,7 @@ mod tests { @ { border_100_50_box(0., 0., 10., 10., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "bottom_left_borders" ); } @@ -702,7 +702,7 @@ mod tests { @ { border_100_50_box(10., 10., 10., 0., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "top_left_and_right_borders" ); @@ -716,7 +716,7 @@ mod tests { @ { border_100_50_box(0., 10., 10., 10., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "right_bottom_and_left_borders" ); @@ -730,7 +730,7 @@ mod tests { @ { border_100_50_box(10., 0., 10., 10., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "bottom_left_and_top_borders" ); } @@ -750,7 +750,7 @@ mod tests { @ { border_100_50_box(10., 10., 10., 10., Some(Radius::all(5.))) } }) .with_wnd_size(Size::new(400., 80.)) - .with_comparison(0.00002), + .with_comparison(0.000065), "all_borders" ); } diff --git a/core/src/builtin_widgets/clip.rs b/core/src/builtin_widgets/clip.rs index 25820e544..bd22086f4 100644 --- a/core/src/builtin_widgets/clip.rs +++ b/core/src/builtin_widgets/clip.rs @@ -1,42 +1,22 @@ use crate::prelude::*; -#[derive(Clone, Default)] -pub enum ClipType { - #[default] - Auto, - Path(Path), -} - -#[derive(SingleChild, Clone, Declare)] +#[derive(SingleChild, Declare)] pub struct Clip { - #[declare(default)] - pub clip: ClipType, + pub clip_path: Path, } impl Render for Clip { - fn only_sized_by_parent(&self) -> bool { false } + fn only_sized_by_parent(&self) -> bool { true } fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { - let child_size = ctx.assert_perform_single_child_layout(clamp); - match self.clip { - ClipType::Auto => child_size, - ClipType::Path(ref path) => path.bounds(None).max().to_tuple().into(), - } + ctx.assert_perform_single_child_layout(clamp); + self + .clip_path + .bounds(None) + .max() + .to_tuple() + .into() } - fn paint(&self, ctx: &mut PaintingCtx) { - let path = match &self.clip { - ClipType::Auto => { - let rect: lyon_geom::euclid::Rect = Rect::from_size( - ctx - .box_rect() - .expect("impossible without size in painting stage") - .size, - ); - Path::rect(&rect) - } - ClipType::Path(path) => path.clone(), - }; - ctx.painter().clip(path.into()); - } + fn paint(&self, ctx: &mut PaintingCtx) { ctx.painter().clip(self.clip_path.clone().into()); } } diff --git a/core/src/builtin_widgets/clip_boundary.rs b/core/src/builtin_widgets/clip_boundary.rs new file mode 100644 index 000000000..cd8f0b686 --- /dev/null +++ b/core/src/builtin_widgets/clip_boundary.rs @@ -0,0 +1,77 @@ +use wrap_render::WrapRender; + +use crate::prelude::*; + +/// This widget use to clip the host widget by the boundary rect with radius. +#[derive(Default, Clone)] +pub struct ClipBoundary { + /// If true, clip the host widget by the boundary rect with radius, else do + /// nothing. + pub clip_boundary: bool, +} + +impl Declare for ClipBoundary { + type Builder = FatObj<()>; + #[inline] + fn declarer() -> Self::Builder { FatObj::new(()) } +} + +impl_compose_child_for_wrap_render!(ClipBoundary, DirtyPhase::Layout); + +impl WrapRender for ClipBoundary { + fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { + host.perform_layout(clamp, ctx) + } + + fn only_sized_by_parent(&self, host: &dyn Render) -> bool { host.only_sized_by_parent() } + + fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) { + if self.clip_boundary { + let rect: lyon_geom::euclid::Rect = Rect::from_size( + ctx + .box_size() + .expect("impossible without size in painting stage"), + ); + let path = if let Some(radius) = Provider::of::(ctx) { + Path::rect_round(&rect, &radius) + } else { + Path::rect(&rect) + }; + + ctx.painter().clip(path.into()); + } + host.paint(ctx) + } +} + +#[cfg(test)] +mod tests { + use ribir_dev_helper::*; + + use super::*; + use crate::{reset_test_env, test_helper::*}; + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn clip_boundary() { + reset_test_env!(); + + let size = Size::new(80., 20.); + assert_widget_eq_image!( + WidgetTester::new(fn_widget! { + @MockBox { + clip_boundary: true, + radius: Radius::all(10.), + size: size, + @MockBox { + background: Color::GRAY, + size: size, + } + } + }) + .with_wnd_size(size) + .with_comparison(0.00015), + "clip_boundary" + ); + } +} diff --git a/core/src/builtin_widgets/scrollable.rs b/core/src/builtin_widgets/scrollable.rs index cbef818b5..e42a96a75 100644 --- a/core/src/builtin_widgets/scrollable.rs +++ b/core/src/builtin_widgets/scrollable.rs @@ -69,9 +69,11 @@ impl<'c> ComposeChild<'c> for ScrollableWidget { .subscribe(move |v| $this.write().set_page(v)); $this.write().view_id = Some($view.track_id()); - @Clip { + + @ $view { + clip_boundary: true, providers: [Provider::value_of_writer(this.clone_boxed_writer(), None)], - @ $view { @ { child } } + @ { child } } } .into_widget() @@ -224,7 +226,7 @@ mod tests { }); wnd.draw_frame(); - let pos = wnd.layout_info_by_path(&[0, 0, 0]).unwrap().pos; + let pos = wnd.layout_info_by_path(&[0, 0]).unwrap().pos; assert_eq!(pos, Point::new(expect_x, expect_y)); } diff --git a/core/src/builtin_widgets/text.rs b/core/src/builtin_widgets/text.rs index cbab93fb3..4c18c5cf9 100644 --- a/core/src/builtin_widgets/text.rs +++ b/core/src/builtin_widgets/text.rs @@ -34,16 +34,18 @@ pub fn text_glyph( pub fn paint_text( painter: &mut Painter, glyphs: &VisualGlyphs, style: PaintingStyle, box_rect: Rect, ) { - if let PaintingStyle::Stroke(options) = style { - painter - .set_style(PathStyle::Stroke) - .set_strokes(options); - } else { - painter.set_style(PathStyle::Fill); - } + if let Some(rect) = painter.intersection_paint_bounds(&box_rect) { + if let PaintingStyle::Stroke(options) = style { + painter + .set_style(PathStyle::Stroke) + .set_strokes(options); + } else { + painter.set_style(PathStyle::Fill); + } - let font_db = AppCtx::font_db().clone(); - painter.draw_glyphs_in_rect(glyphs, box_rect, &font_db.borrow()); + let font_db = AppCtx::font_db().clone(); + painter.draw_glyphs_in_rect(glyphs, rect, &font_db.borrow()); + } } impl Render for Text { @@ -81,7 +83,8 @@ impl Render for Text { let style = Provider::of::(ctx).map(|p| p.clone()); let visual_glyphs = self.glyphs().unwrap(); - paint_text(ctx.painter(), &visual_glyphs, style.unwrap_or(PaintingStyle::Fill), box_rect); + let rect = visual_glyphs.visual_rect(); + paint_text(ctx.painter(), &visual_glyphs, style.unwrap_or(PaintingStyle::Fill), rect); } } @@ -134,6 +137,7 @@ mod tests { WidgetTester::new(fn_widget! { @MockBox { size: Size::new(50., 45.), + clip_boundary: true, @Text { text: "hello world,\rnice to meet you.", } @@ -171,12 +175,13 @@ mod tests { } @Text { text: "Text line height check!", + clip_boundary: true, font_size: 20., text_line_height: 40., background: Color::GREEN, } }) .with_wnd_size(WND_SIZE) - .with_comparison(0.00004) + .with_comparison(0.00009) ); } diff --git a/core/src/builtin_widgets/theme.rs b/core/src/builtin_widgets/theme.rs index a76312f28..208af038e 100644 --- a/core/src/builtin_widgets/theme.rs +++ b/core/src/builtin_widgets/theme.rs @@ -235,7 +235,7 @@ fn typography_theme() -> TypographyTheme { weight: FontWeight::NORMAL, ..<_>::default() }; - let overflow = TextOverflow::Clip; + let overflow = TextOverflow::Overflow; TextTheme { text: TextStyle { line_height, font_size, letter_space, font_face, overflow }, decoration: TextDecorationStyle { diff --git a/macros/src/variable_names.rs b/macros/src/variable_names.rs index a4ee0c78b..5ad3adc90 100644 --- a/macros/src/variable_names.rs +++ b/macros/src/variable_names.rs @@ -174,4 +174,8 @@ pub static BUILTIN_INFOS: phf::Map<&'static str, BuiltinMember> = phf_map! { "tooltips" => builtin_member!{"Tooltips", Field, "tooltips"}, // TrackWidgetId "track_id" => builtin_member!{"TrackWidgetId", Method, "track_id"}, + // ClipBoundary + "clip_boundary" => builtin_member!{"ClipBoundary", Field, "clip_boundary"}, + // Providers + "providers" => builtin_member!{"Providers", Field, "providers"}, }; diff --git a/painter/src/painter.rs b/painter/src/painter.rs index 9f905300f..c97a1f427 100644 --- a/painter/src/painter.rs +++ b/painter/src/painter.rs @@ -681,9 +681,6 @@ impl Painter { return self; }; - if !paint_rect.contains_rect(&visual_rect) { - self.clip(Path::rect(&paint_rect).into()); - } self.translate(visual_rect.origin.x, visual_rect.origin.y); for g in glyphs { diff --git a/painter/src/text.rs b/painter/src/text.rs index 0b9ed3fc1..e3973ee73 100644 --- a/painter/src/text.rs +++ b/painter/src/text.rs @@ -88,7 +88,7 @@ pub struct TextStyle { #[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)] pub enum TextOverflow { #[default] - Clip, + Overflow, AutoWrap, } diff --git a/painter/src/text/typography_store.rs b/painter/src/text/typography_store.rs index 9ccd0f9f9..9f3eb2e0b 100644 --- a/painter/src/text/typography_store.rs +++ b/painter/src/text/typography_store.rs @@ -469,7 +469,7 @@ impl TypographyKey { let line_width = match overflow { // line width is not so important in clip mode, the cache can be use even with difference line // width. The wider one can use for the narrower one. S - TextOverflow::Clip => GlyphUnit::MAX, + TextOverflow::Overflow => GlyphUnit::MAX, TextOverflow::AutoWrap => { if line_dir.is_horizontal() { @@ -525,7 +525,7 @@ mod tests { world!" .into(); - let style = zero_letter_space_style(14., TextOverflow::Clip); + let style = zero_letter_space_style(14., TextOverflow::Overflow); let visual = typography_text( text, &style, @@ -541,7 +541,7 @@ mod tests { fn empty_text_bounds() { let text = "".into(); - let style = zero_letter_space_style(14., TextOverflow::Clip); + let style = zero_letter_space_style(14., TextOverflow::Overflow); let visual = typography_text( text, &style, @@ -556,7 +556,7 @@ mod tests { #[test] fn new_line_bounds() { let text = "123\n".into(); - let style = zero_letter_space_style(14., TextOverflow::Clip); + let style = zero_letter_space_style(14., TextOverflow::Overflow); let visual = typography_text( text, &style, @@ -589,7 +589,7 @@ mod tests { } let not_bounds = glyphs( - TextOverflow::Clip, + TextOverflow::Overflow, Size::new(f32::MAX, f32::MAX), TextAlign::Start, PlaceLineDirection::TopToBottom, @@ -619,7 +619,7 @@ mod tests { ]); let r_align = glyphs( - TextOverflow::Clip, + TextOverflow::Overflow, Size::new(100., f32::MAX), TextAlign::End, PlaceLineDirection::TopToBottom, @@ -649,7 +649,7 @@ mod tests { ],); let bottom = glyphs( - TextOverflow::Clip, + TextOverflow::Overflow, Size::new(100., 100.), TextAlign::Start, PlaceLineDirection::BottomToTop, @@ -681,7 +681,7 @@ mod tests { ],); let center_clip = glyphs( - TextOverflow::Clip, + TextOverflow::Overflow, Size::new(40., 15.), TextAlign::Center, PlaceLineDirection::TopToBottom, @@ -711,7 +711,7 @@ mod tests { let text: Substr = "hi!".into(); - let style = zero_letter_space_style(16., TextOverflow::Clip); + let style = zero_letter_space_style(16., TextOverflow::Overflow); assert!(store.cache.is_empty()); @@ -747,7 +747,7 @@ mod tests { #[test] fn cluster_position() { - let style = zero_letter_space_style(15., TextOverflow::Clip); + let style = zero_letter_space_style(15., TextOverflow::Overflow); let text = "abcd \u{202e} right_to_left_1 \u{202d} embed \u{202c} right_to_left_2 \u{202c} end".into(); let glyphs = typography_text( @@ -805,7 +805,7 @@ mod tests { let mut store = test_store(); let text: Substr = "1234".into(); - let style = zero_letter_space_style(16., TextOverflow::Clip); + let style = zero_letter_space_style(16., TextOverflow::Overflow); let glyphs1 = store.typography( text.clone(), &style, diff --git a/test_cases/ribir_core/builtin_widgets/clip_boundary/tests/clip_boundary.png b/test_cases/ribir_core/builtin_widgets/clip_boundary/tests/clip_boundary.png new file mode 100644 index 0000000000000000000000000000000000000000..21b624daf37c85e6b1b55dfc382f15fc49e0b650 GIT binary patch literal 663 zcmeAS@N?(olHy`uVBq!ia0vp^AS}kg1|)-@o_@%{z!c}{;uuoF`1S$g1QpLoDxN~! zt7h(YcX3HeOA8AHf^rb}eufsH1Y~r0ILMZ?G&eVwmff>wa-H`{3okD%FZVwWveGAM zZrI{VySn%Mb@C1BHZM8nSeN(x&-a^e$9v#q@bbzti{;L~yu949_LYe~!z`duprVhQ zf3BSWXA*9^{PJ@D&r<7uy|Vcg*#GRO!{scK^Ixj#Km7jlPyU}>_}+x||5mL(m%ZoK z>d!GNY%UAW`@vH8pTBbM>E{~x~3{=hWu?bS*KrU#t6}{oLrR#oO2l8u68UP1CA;J8iMcuZs`1!fQtHo7cfriCBz8W|8IkWHT zi#GE96_11~{s_-Mu=DeC28;4{GXL*`-0N=t>Eawq5dG@n9)XOg-e;@o^|iwPx<8O- ZocggsU_trSd|-lO@O1TaS?83{1OPs2Y#jgq literal 0 HcmV?d00001 diff --git a/themes/material/src/lib.rs b/themes/material/src/lib.rs index 62d84bb55..efa83a96c 100644 --- a/themes/material/src/lib.rs +++ b/themes/material/src/lib.rs @@ -246,7 +246,7 @@ pub fn typography_theme() -> TypographyTheme { font_size, letter_space, font_face, - overflow: TextOverflow::Clip, + overflow: TextOverflow::Overflow, }, decoration: TextDecorationStyle { decoration: TextDecoration::NONE, diff --git a/themes/ribir_slim/src/lib.rs b/themes/ribir_slim/src/lib.rs index 272afa6fd..f6fdd3d1c 100644 --- a/themes/ribir_slim/src/lib.rs +++ b/themes/ribir_slim/src/lib.rs @@ -81,7 +81,7 @@ fn typography_theme() -> TypographyTheme { font_size, letter_space, font_face, - overflow: TextOverflow::Clip, + overflow: TextOverflow::Overflow, }, decoration: TextDecorationStyle { decoration: TextDecoration::NONE, diff --git a/widgets/src/avatar.rs b/widgets/src/avatar.rs index c3d2741da..1d81f5137 100644 --- a/widgets/src/avatar.rs +++ b/widgets/src/avatar.rs @@ -75,6 +75,7 @@ impl ComposeChild<'static> for Avatar { } @ $container { background: pipe!(Brush::from(palette1.base_of(&$this.color))), + clip_boundary: true, @Text { h_align: HAlign::Center, v_align: VAlign::Center, @@ -91,7 +92,7 @@ impl ComposeChild<'static> for Avatar { &Rect::from_size(size), &Radius::all(radius), ); - Clip { clip: ClipType::Path(path) } + Clip { clip_path: path } }); @$clip { @Container { diff --git a/widgets/src/lists.rs b/widgets/src/lists.rs index 95bd573b1..217eda059 100644 --- a/widgets/src/lists.rs +++ b/widgets/src/lists.rs @@ -285,6 +285,7 @@ impl<'c> ComposeChild<'c> for ListItem { flex: 1., @ $label_gap { @Column { + clip_boundary: true, @Text { text: headline.0.0, foreground: Palette::of(ctx).on_surface(),