Clip behavior card labels to their bounds
This commit is contained in:
@@ -1069,6 +1069,17 @@ struct BoxItem {
|
||||
detail: String,
|
||||
marker: Option<Kind>,
|
||||
}
|
||||
fn box_text_clip(rect: Rectangle, scale: f32, viewport: Size) -> Option<Rectangle> {
|
||||
if !scale.is_finite() || scale <= 0. || rect.width <= 20. || rect.height <= 16. {
|
||||
return None;
|
||||
}
|
||||
Rectangle::new(
|
||||
Point::new((rect.x + 10.) * scale, (rect.y + 8.) * scale),
|
||||
Size::new((rect.width - 20.) * scale, (rect.height - 16.) * scale),
|
||||
)
|
||||
.intersection(&Rectangle::new(Point::ORIGIN, viewport))
|
||||
}
|
||||
|
||||
struct Line {
|
||||
id: Id,
|
||||
points: Vec<Point>,
|
||||
@@ -2134,24 +2145,31 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
}
|
||||
for b in &self.boxes {
|
||||
f.fill_text(Text {
|
||||
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
|
||||
position: Point::new(b.rect.x + 12., b.rect.y + 12.),
|
||||
size: 13.into(),
|
||||
color: if b.id == self.selected {
|
||||
style::TEAL
|
||||
} else {
|
||||
style::INK
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
if !b.detail.is_empty() {
|
||||
f.fill_text(Text {
|
||||
content: short(&b.detail, ((b.rect.width - 25.) / 6.) as usize),
|
||||
position: Point::new(b.rect.x + 12., b.rect.y + 39.),
|
||||
size: 11.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
if let Some(clip) = box_text_clip(b.rect, self.scale, f.size()) {
|
||||
// Clipped child frames start with an identity transform. Match
|
||||
// the card's scale explicitly, as for lifecycle Create labels.
|
||||
f.with_clip(clip, |label| {
|
||||
label.scale(self.scale);
|
||||
label.fill_text(Text {
|
||||
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
|
||||
position: Point::new(b.rect.x + 12., b.rect.y + 12.),
|
||||
size: 13.into(),
|
||||
color: if b.id == self.selected {
|
||||
style::TEAL
|
||||
} else {
|
||||
style::INK
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
if !b.detail.is_empty() {
|
||||
label.fill_text(Text {
|
||||
content: short(&b.detail, ((b.rect.width - 25.) / 6.) as usize),
|
||||
position: Point::new(b.rect.x + 12., b.rect.y + 39.),
|
||||
size: 11.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,116 @@ fn intervals(scene: &Scene, lifeline: Id, scope: Id) -> Vec<(f32, f32, Life)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_long_header_title_and_detail_keep_names_and_have_bounded_clips() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for name in [
|
||||
"W".repeat(18),
|
||||
"界".repeat(24),
|
||||
"e\u{301}".repeat(24),
|
||||
"👩\u{200d}🔧".repeat(12),
|
||||
] {
|
||||
let mut f = Fixture::new(layer);
|
||||
f.project.elements.get_mut(&f.receiver).unwrap().name = name.clone();
|
||||
f.project.elements.get_mut(&f.work).unwrap().name = name.clone();
|
||||
let scene = f.scene();
|
||||
let header = scene.boxes.iter().find(|b| b.id == f.receiver).unwrap();
|
||||
assert_eq!(header.title, format!("Lane · {name}"));
|
||||
assert_eq!(header.detail, name);
|
||||
assert_eq!(header.rect.size(), Size::new(204., 70.));
|
||||
assert_eq!(scene.hit(header.rect.center()), Some(f.receiver));
|
||||
// Scroll offset is handled by the existing outer canvas/scrollable;
|
||||
// these are the local drawing frame's coordinates, not screen input.
|
||||
for scale in [0.05, 0.5, 0.512, 0.64, 0.8, 1., 1.953125, 2.] {
|
||||
let clip =
|
||||
crate::behavior::box_text_clip(header.rect, scale, Size::new(2000., 2000.))
|
||||
.unwrap();
|
||||
let world = Rectangle::new(
|
||||
Point::new(clip.x / scale, clip.y / scale),
|
||||
Size::new(clip.width / scale, clip.height / scale),
|
||||
);
|
||||
assert!((world.x - header.rect.x - 10.).abs() < 0.001);
|
||||
assert!((world.y - header.rect.y - 8.).abs() < 0.001);
|
||||
assert!((world.width - 184.).abs() < 0.001);
|
||||
assert!((world.height - 54.).abs() < 0.001);
|
||||
assert!(header.rect.contains(world.position()));
|
||||
assert!(
|
||||
header
|
||||
.rect
|
||||
.contains(Point::new(world.x + world.width, world.y + world.height,))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_text_clip_intersects_parent_viewport_and_rejects_empty_interiors() {
|
||||
use crate::behavior::box_text_clip;
|
||||
let card = Rectangle::new(Point::new(100., 200.), Size::new(204., 70.));
|
||||
assert_eq!(
|
||||
box_text_clip(card, 2., Size::new(300., 500.)),
|
||||
Some(Rectangle::new(Point::new(220., 416.), Size::new(80., 84.)))
|
||||
);
|
||||
assert_eq!(
|
||||
box_text_clip(
|
||||
Rectangle::new(Point::new(-20., -10.), card.size()),
|
||||
2.,
|
||||
Size::new(300., 500.),
|
||||
),
|
||||
Some(Rectangle::new(Point::ORIGIN, Size::new(300., 104.)))
|
||||
);
|
||||
for viewport in [Size::new(200., 500.), Size::new(300., 400.), Size::ZERO] {
|
||||
assert_eq!(box_text_clip(card, 2., viewport), None);
|
||||
}
|
||||
for scale in [0., -1., f32::NAN, f32::INFINITY] {
|
||||
assert_eq!(box_text_clip(card, scale, Size::new(1000., 1000.)), None);
|
||||
}
|
||||
for size in [Size::new(20., 70.), Size::new(204., 16.), Size::ZERO] {
|
||||
assert_eq!(
|
||||
box_text_clip(Rectangle::new(Point::ORIGIN, size), 1., card.size()),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_wide_latin_header_exceeds_old_character_budget() {
|
||||
use iced::advanced::text::{self, Paragraph as _};
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
f.project.elements.get_mut(&f.receiver).unwrap().name = "W".repeat(18);
|
||||
f.project.elements.get_mut(&f.work).unwrap().name = "W".repeat(24);
|
||||
let scene = f.scene();
|
||||
let header = scene.boxes.iter().find(|b| b.id == f.receiver).unwrap();
|
||||
for (content, size, divisor) in [(&header.title, 13., 7.), (&header.detail, 11., 6.)] {
|
||||
let label = iced::widget::canvas::Text {
|
||||
content: crate::views::short(content, ((header.rect.width - 25.) / divisor) as usize),
|
||||
size: size.into(),
|
||||
..Default::default()
|
||||
};
|
||||
// Use the same real paragraph/font/shaping defaults as canvas Text.
|
||||
// The native defect survives short(): glyph width, not scalar count,
|
||||
// is what exceeds the card. This is a font witness, not a pixel gate.
|
||||
let paragraph = <iced::Renderer as text::Renderer>::Paragraph::with_text(text::Text {
|
||||
content: label.content.as_str(),
|
||||
bounds: Size::INFINITE,
|
||||
size: label.size,
|
||||
line_height: label.line_height,
|
||||
font: label.font,
|
||||
align_x: label.align_x,
|
||||
align_y: label.align_y,
|
||||
shaping: label.shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
});
|
||||
let old_available = header.rect.width - 12.;
|
||||
assert!(
|
||||
paragraph.min_width() > old_available,
|
||||
"wide glyph fixture must overflow the old unbounded card: {} <= {old_available}",
|
||||
paragraph.min_width()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn birth_label_clip_keeps_padded_world_bounds_at_each_scene_scale() {
|
||||
let capsule = Rectangle::new(Point::new(100., 200.), Size::new(140., 28.));
|
||||
|
||||
Reference in New Issue
Block a user