Port example 03
This commit is contained in:
@@ -25,3 +25,7 @@ path = "example/01_async_data_and_effects.rs"
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "02_widget_refs_and_commands"
|
name = "02_widget_refs_and_commands"
|
||||||
path = "example/02_widget_refs_and_commands.rs"
|
path = "example/02_widget_refs_and_commands.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "03_fine_grained_list"
|
||||||
|
path = "example/03_fine_grained_list.rs"
|
||||||
|
|||||||
512
lib/ruin_app/example/03_fine_grained_list.rs
Normal file
512
lib/ruin_app/example/03_fine_grained_list.rs
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
use ruin_app::prelude::*;
|
||||||
|
|
||||||
|
#[ruin_runtime::async_main]
|
||||||
|
async fn main() -> ruin_app::Result<()> {
|
||||||
|
App::new()
|
||||||
|
.window(
|
||||||
|
Window::new()
|
||||||
|
.title("RUIN Tasks")
|
||||||
|
.app_id("dev.ruin.fine-grained-list")
|
||||||
|
.size(1180.0, 780.0),
|
||||||
|
)
|
||||||
|
.mount(view! {
|
||||||
|
TaskBoard() {}
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn TaskBoard() -> impl IntoView {
|
||||||
|
let filter = use_signal(|| TaskFilter::All);
|
||||||
|
let tasks = use_signal(seed_tasks);
|
||||||
|
let list_scroll = use_signal(|| 0.0_f32);
|
||||||
|
|
||||||
|
let total_count = use_memo({
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move || tasks.with(|tasks| tasks.len())
|
||||||
|
});
|
||||||
|
let open_count = use_memo({
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move || {
|
||||||
|
tasks.with(|tasks| {
|
||||||
|
tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|task| !matches!(task.status, TaskStatus::Done))
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let done_count = use_memo({
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move || {
|
||||||
|
tasks.with(|tasks| {
|
||||||
|
tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|task| matches!(task.status, TaskStatus::Done))
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let visible_ids = use_memo({
|
||||||
|
let filter = filter.clone();
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move || {
|
||||||
|
tasks.with(|tasks| {
|
||||||
|
tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|task| filter.get().matches(task.status))
|
||||||
|
.map(|task| task.id)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
use_window_title({
|
||||||
|
let visible_ids = visible_ids.clone();
|
||||||
|
let total_count = total_count.clone();
|
||||||
|
move || {
|
||||||
|
format!(
|
||||||
|
"RUIN Tasks ({}/{})",
|
||||||
|
visible_ids.with(|ids| ids.len()),
|
||||||
|
total_count.get()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let all_label = if matches!(filter.get(), TaskFilter::All) {
|
||||||
|
"● All"
|
||||||
|
} else {
|
||||||
|
"○ All"
|
||||||
|
};
|
||||||
|
let open_label = if matches!(filter.get(), TaskFilter::OpenOnly) {
|
||||||
|
"● Open"
|
||||||
|
} else {
|
||||||
|
"○ Open"
|
||||||
|
};
|
||||||
|
let done_label = if matches!(filter.get(), TaskFilter::CompletedOnly) {
|
||||||
|
"● Completed"
|
||||||
|
} else {
|
||||||
|
"○ Completed"
|
||||||
|
};
|
||||||
|
let task_rows = visible_ids.with(|ids| {
|
||||||
|
ids.iter()
|
||||||
|
.map(|task_id| {
|
||||||
|
TaskRow::builder()
|
||||||
|
.tasks(tasks.clone())
|
||||||
|
.task_id(*task_id)
|
||||||
|
.children(())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
column(gap = 16.0, padding = 24.0) {
|
||||||
|
text(role = TextRole::Heading(1), size = 30.0, weight = FontWeight::Semibold) {
|
||||||
|
"Fine-grained list updates"
|
||||||
|
}
|
||||||
|
|
||||||
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
||||||
|
"This is an honest slice of aspirational example 03: filtering, row mutation, \
|
||||||
|
reordering, and wrapped-list reflow all work end-to-end. True keyed diffing and \
|
||||||
|
layout-island invalidation are still future work, so this example exercises the \
|
||||||
|
real runtime we have rather than pretending those optimizations already exist."
|
||||||
|
}
|
||||||
|
|
||||||
|
row(gap = 8.0) {
|
||||||
|
button(on_press = {
|
||||||
|
let filter = filter.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = filter.set(TaskFilter::All);
|
||||||
|
}
|
||||||
|
}) { all_label }
|
||||||
|
|
||||||
|
button(on_press = {
|
||||||
|
let filter = filter.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = filter.set(TaskFilter::OpenOnly);
|
||||||
|
}
|
||||||
|
}) { open_label }
|
||||||
|
|
||||||
|
button(on_press = {
|
||||||
|
let filter = filter.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = filter.set(TaskFilter::CompletedOnly);
|
||||||
|
}
|
||||||
|
}) { done_label }
|
||||||
|
|
||||||
|
button(on_press = {
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move |_| {
|
||||||
|
tasks.replace(seed_tasks());
|
||||||
|
}
|
||||||
|
}) { "Reset board" }
|
||||||
|
}
|
||||||
|
|
||||||
|
row(gap = 16.0) {
|
||||||
|
block(
|
||||||
|
flex = 1.0,
|
||||||
|
padding = 16.0,
|
||||||
|
gap = 12.0,
|
||||||
|
background = surfaces::raised(),
|
||||||
|
border_radius = 12.0,
|
||||||
|
) {
|
||||||
|
text(size = 18.0, weight = FontWeight::Semibold) { "Tasks" }
|
||||||
|
|
||||||
|
text(color = colors::muted()) {
|
||||||
|
visible_ids.with(|ids| ids.len());
|
||||||
|
" visible · ";
|
||||||
|
total_count.clone();
|
||||||
|
" total"
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_box(
|
||||||
|
offset_y = list_scroll.clone(),
|
||||||
|
height = 520.0,
|
||||||
|
padding = 12.0,
|
||||||
|
background = surfaces::canvas(),
|
||||||
|
border_radius = 10.0,
|
||||||
|
border = (2.0, colors::muted()),
|
||||||
|
) {
|
||||||
|
column(gap = 10.0) {
|
||||||
|
task_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
block(
|
||||||
|
width = 300.0,
|
||||||
|
padding = 16.0,
|
||||||
|
gap = 12.0,
|
||||||
|
background = surfaces::raised(),
|
||||||
|
border_radius = 12.0,
|
||||||
|
) {
|
||||||
|
text(size = 18.0, weight = FontWeight::Semibold) { "Board summary" }
|
||||||
|
|
||||||
|
block(
|
||||||
|
padding = 12.0,
|
||||||
|
gap = 8.0,
|
||||||
|
background = surfaces::canvas(),
|
||||||
|
border_radius = 10.0,
|
||||||
|
) {
|
||||||
|
text() { "Open tasks: "; open_count.clone() }
|
||||||
|
text() { "Completed tasks: "; done_count.clone() }
|
||||||
|
text() { "Current filter: "; filter.get().label() }
|
||||||
|
}
|
||||||
|
|
||||||
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
||||||
|
"Each task row can advance its status, expand notes, and move within the \
|
||||||
|
backing list. The scroll box is intentional here: resizing the window \
|
||||||
|
changes wrapping pressure, which is useful for validating list reflow and \
|
||||||
|
scroll clamping under real content."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn TaskRow(tasks: Signal<Vec<Task>>, task_id: u64) -> impl IntoView {
|
||||||
|
let task = tasks.with(|tasks| {
|
||||||
|
tasks
|
||||||
|
.iter()
|
||||||
|
.find(|task| task.id == task_id)
|
||||||
|
.cloned()
|
||||||
|
.expect("task row should only render live tasks")
|
||||||
|
});
|
||||||
|
let task_index = tasks.with(|tasks| {
|
||||||
|
tasks
|
||||||
|
.iter()
|
||||||
|
.position(|task| task.id == task_id)
|
||||||
|
.expect("task row should know its current index")
|
||||||
|
});
|
||||||
|
let can_move_up = task_index > 0;
|
||||||
|
let can_move_down = tasks.with(|tasks| task_index + 1 < tasks.len());
|
||||||
|
|
||||||
|
let mut detail_views = vec![
|
||||||
|
text()
|
||||||
|
.weight(if matches!(task.status, TaskStatus::Done) {
|
||||||
|
FontWeight::Medium
|
||||||
|
} else {
|
||||||
|
FontWeight::Semibold
|
||||||
|
})
|
||||||
|
.children(task.title.clone()),
|
||||||
|
text().color(colors::muted()).children((
|
||||||
|
"Owner: ",
|
||||||
|
task.owner.clone(),
|
||||||
|
" · Phase: ",
|
||||||
|
task.status.description(),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
if task.expanded {
|
||||||
|
detail_views.push(
|
||||||
|
text()
|
||||||
|
.color(colors::muted())
|
||||||
|
.wrap(TextWrap::Word)
|
||||||
|
.children(task.notes.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut action_views = vec![
|
||||||
|
button()
|
||||||
|
.on_press({
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move |_| {
|
||||||
|
update_task(&tasks, task_id, |task| {
|
||||||
|
task.status = task.status.advance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.children(task.status.advance_label()),
|
||||||
|
button()
|
||||||
|
.on_press({
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move |_| {
|
||||||
|
update_task(&tasks, task_id, |task| {
|
||||||
|
task.expanded = !task.expanded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.children(if task.expanded {
|
||||||
|
"Hide notes"
|
||||||
|
} else {
|
||||||
|
"Show notes"
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
if can_move_up {
|
||||||
|
action_views.push(
|
||||||
|
button()
|
||||||
|
.on_press({
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move |_| move_task(&tasks, task_id, MoveDirection::Up)
|
||||||
|
})
|
||||||
|
.children("Move up"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if can_move_down {
|
||||||
|
action_views.push(
|
||||||
|
button()
|
||||||
|
.on_press({
|
||||||
|
let tasks = tasks.clone();
|
||||||
|
move |_| move_task(&tasks, task_id, MoveDirection::Down)
|
||||||
|
})
|
||||||
|
.children("Move down"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
block(
|
||||||
|
padding = 14.0,
|
||||||
|
gap = 12.0,
|
||||||
|
background = task.status.background(),
|
||||||
|
border_radius = 12.0,
|
||||||
|
border = (1.0, task.status.accent()),
|
||||||
|
) {
|
||||||
|
row(gap = 12.0) {
|
||||||
|
block(
|
||||||
|
width = 120.0,
|
||||||
|
padding = 10.0,
|
||||||
|
background = surfaces::canvas(),
|
||||||
|
border_radius = 10.0,
|
||||||
|
border = (1.0, task.status.accent()),
|
||||||
|
) {
|
||||||
|
text(weight = FontWeight::Semibold, color = task.status.accent()) {
|
||||||
|
task.status.label()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
column(flex = 1.0, gap = 8.0) {
|
||||||
|
detail_views
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row(gap = 8.0) {
|
||||||
|
action_views
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Task {
|
||||||
|
id: u64,
|
||||||
|
title: String,
|
||||||
|
owner: String,
|
||||||
|
notes: String,
|
||||||
|
status: TaskStatus,
|
||||||
|
expanded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum MoveDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum TaskStatus {
|
||||||
|
Backlog,
|
||||||
|
Doing,
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskStatus {
|
||||||
|
const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TaskStatus::Backlog => "Backlog",
|
||||||
|
TaskStatus::Doing => "Doing",
|
||||||
|
TaskStatus::Done => "Done",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn description(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TaskStatus::Backlog => "Waiting for a pass",
|
||||||
|
TaskStatus::Doing => "Actively being verified",
|
||||||
|
TaskStatus::Done => "Ready to ship",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn advance(self) -> Self {
|
||||||
|
match self {
|
||||||
|
TaskStatus::Backlog => TaskStatus::Doing,
|
||||||
|
TaskStatus::Doing => TaskStatus::Done,
|
||||||
|
TaskStatus::Done => TaskStatus::Backlog,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn advance_label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TaskStatus::Backlog => "Start work",
|
||||||
|
TaskStatus::Doing => "Complete",
|
||||||
|
TaskStatus::Done => "Reopen",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn accent(self) -> Color {
|
||||||
|
match self {
|
||||||
|
TaskStatus::Backlog => Color::rgb(0xE3, 0xB5, 0x65),
|
||||||
|
TaskStatus::Doing => Color::rgb(0x71, 0xA7, 0xF7),
|
||||||
|
TaskStatus::Done => Color::rgb(0x69, 0xC3, 0x7D),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn background(self) -> Color {
|
||||||
|
match self {
|
||||||
|
TaskStatus::Backlog => Color::rgb(0x38, 0x2B, 0x1C),
|
||||||
|
TaskStatus::Doing => Color::rgb(0x1F, 0x2E, 0x49),
|
||||||
|
TaskStatus::Done => Color::rgb(0x1E, 0x36, 0x28),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum TaskFilter {
|
||||||
|
All,
|
||||||
|
OpenOnly,
|
||||||
|
CompletedOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskFilter {
|
||||||
|
const fn matches(self, status: TaskStatus) -> bool {
|
||||||
|
match self {
|
||||||
|
TaskFilter::All => true,
|
||||||
|
TaskFilter::OpenOnly => !matches!(status, TaskStatus::Done),
|
||||||
|
TaskFilter::CompletedOnly => matches!(status, TaskStatus::Done),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TaskFilter::All => "All",
|
||||||
|
TaskFilter::OpenOnly => "Open only",
|
||||||
|
TaskFilter::CompletedOnly => "Completed only",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_task(tasks: &Signal<Vec<Task>>, task_id: u64, mutate: impl FnOnce(&mut Task)) {
|
||||||
|
tasks.update(|tasks| {
|
||||||
|
if let Some(task) = tasks.iter_mut().find(|task| task.id == task_id) {
|
||||||
|
mutate(task);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_task(tasks: &Signal<Vec<Task>>, task_id: u64, direction: MoveDirection) {
|
||||||
|
tasks.update(|tasks| {
|
||||||
|
let Some(index) = tasks.iter().position(|task| task.id == task_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let target = match direction {
|
||||||
|
MoveDirection::Up if index > 0 => index - 1,
|
||||||
|
MoveDirection::Down if index + 1 < tasks.len() => index + 1,
|
||||||
|
_ => index,
|
||||||
|
};
|
||||||
|
if target != index {
|
||||||
|
tasks.swap(index, target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seed_tasks() -> Vec<Task> {
|
||||||
|
vec![
|
||||||
|
Task {
|
||||||
|
id: 1,
|
||||||
|
title: "Audit nested clip propagation".to_string(),
|
||||||
|
owner: "Renderer".to_string(),
|
||||||
|
notes: "Confirm that child rounded clips preserve parent clip intersections, including the empty-intersection case that previously let fully offscreen text reappear.".to_string(),
|
||||||
|
status: TaskStatus::Done,
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
|
Task {
|
||||||
|
id: 2,
|
||||||
|
title: "Clamp stale scroll offsets on reflow".to_string(),
|
||||||
|
owner: "Layout".to_string(),
|
||||||
|
notes: "When wrapped content gets shorter after a resize, the scrollbox should clamp the old offset before positioning children so the viewport never opens up an empty gap.".to_string(),
|
||||||
|
status: TaskStatus::Done,
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
|
Task {
|
||||||
|
id: 3,
|
||||||
|
title: "Prototype honest example 03 slice".to_string(),
|
||||||
|
owner: "ruin_app".to_string(),
|
||||||
|
notes: "Keep the example faithful to the current runtime: task filtering, list reordering, and row mutation should work today even though keyed diffing and layout islands are still future work.".to_string(),
|
||||||
|
status: TaskStatus::Doing,
|
||||||
|
expanded: true,
|
||||||
|
},
|
||||||
|
Task {
|
||||||
|
id: 4,
|
||||||
|
title: "Add vector child composition".to_string(),
|
||||||
|
owner: "Macros".to_string(),
|
||||||
|
notes: "Allow `Vec<View>`-style composition so examples can build honest dynamic lists without inventing fake `for` syntax before the macro grammar is ready.".to_string(),
|
||||||
|
status: TaskStatus::Doing,
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
|
Task {
|
||||||
|
id: 5,
|
||||||
|
title: "Stress wrapped task notes".to_string(),
|
||||||
|
owner: "Examples".to_string(),
|
||||||
|
notes: "Task notes should be long enough to force wrapping and scroll-box growth so resize/reflow behavior is easy to observe while manually testing the example.".to_string(),
|
||||||
|
status: TaskStatus::Backlog,
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
|
Task {
|
||||||
|
id: 6,
|
||||||
|
title: "Decide the keyed-list surface".to_string(),
|
||||||
|
owner: "Design".to_string(),
|
||||||
|
notes: "Later passes still need a real keyed list story, stable nested component identity, and layout invalidation boundaries rather than whole-tree rerenders.".to_string(),
|
||||||
|
status: TaskStatus::Backlog,
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
|
Task {
|
||||||
|
id: 7,
|
||||||
|
title: "Polish button variants".to_string(),
|
||||||
|
owner: "Design".to_string(),
|
||||||
|
notes: "The current button surface is deliberately minimal. Example 03 would read even better once button kinds and denser inline controls exist.".to_string(),
|
||||||
|
status: TaskStatus::Backlog,
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -499,6 +499,12 @@ impl<T: IntoView> Children for T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: IntoView> Children for Vec<T> {
|
||||||
|
fn into_views(self) -> Vec<View> {
|
||||||
|
self.into_iter().map(IntoView::into_view).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! impl_children_tuple {
|
macro_rules! impl_children_tuple {
|
||||||
($($name:ident),+ $(,)?) => {
|
($($name:ident),+ $(,)?) => {
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
|
|||||||
@@ -2135,7 +2135,11 @@ fn push_clip_state(stack: &mut Vec<ActiveClip>, active: &mut ActiveClip, region:
|
|||||||
active.rounded = Some((region.rect, region.radius));
|
active.rounded = Some((region.rect, region.radius));
|
||||||
}
|
}
|
||||||
active.rect = if active.rect_active {
|
active.rect = if active.rect_active {
|
||||||
intersect_rects(active.rect, Some(region.rect))
|
if active.rect.is_none() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
intersect_rects(active.rect, Some(region.rect))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Some(region.rect)
|
Some(region.rect)
|
||||||
};
|
};
|
||||||
@@ -2397,4 +2401,47 @@ mod tests {
|
|||||||
.is_none()
|
.is_none()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deeper_nested_clip_cannot_revive_empty_parent_clip() {
|
||||||
|
let mut clip_stack = Vec::new();
|
||||||
|
let mut active_clip = ActiveClip::default();
|
||||||
|
|
||||||
|
push_clip_state(
|
||||||
|
&mut clip_stack,
|
||||||
|
&mut active_clip,
|
||||||
|
ClipRegion {
|
||||||
|
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
|
||||||
|
radius: 0.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
push_clip_state(
|
||||||
|
&mut clip_stack,
|
||||||
|
&mut active_clip,
|
||||||
|
ClipRegion {
|
||||||
|
rect: Rect::new(150.0, 150.0, 50.0, 50.0),
|
||||||
|
radius: 12.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
push_clip_state(
|
||||||
|
&mut clip_stack,
|
||||||
|
&mut active_clip,
|
||||||
|
ClipRegion {
|
||||||
|
rect: Rect::new(160.0, 160.0, 24.0, 12.0),
|
||||||
|
radius: 8.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(active_clip.rect_active);
|
||||||
|
assert_eq!(active_clip.rect, None);
|
||||||
|
assert!(
|
||||||
|
build_text_vertices(
|
||||||
|
Point::new(162.0, 162.0),
|
||||||
|
UiSize::new(16.0, 8.0),
|
||||||
|
UiSize::new(400.0, 400.0),
|
||||||
|
active_clip,
|
||||||
|
)
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user