Scroll box example
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,12 @@ pub enum PointerButton {
|
|||||||
Middle,
|
Middle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum PointerEventKind {
|
pub enum PointerEventKind {
|
||||||
Move,
|
Move,
|
||||||
Down { button: PointerButton },
|
Down { button: PointerButton },
|
||||||
Up { button: PointerButton },
|
Up { button: PointerButton },
|
||||||
|
Scroll { delta: Point },
|
||||||
LeaveWindow,
|
LeaveWindow,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,13 +33,14 @@ impl PointerEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum RoutedPointerEventKind {
|
pub enum RoutedPointerEventKind {
|
||||||
Enter,
|
Enter,
|
||||||
Leave,
|
Leave,
|
||||||
Move,
|
Move,
|
||||||
Down { button: PointerButton },
|
Down { button: PointerButton },
|
||||||
Up { button: PointerButton },
|
Up { button: PointerButton },
|
||||||
|
Scroll { delta: Point },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -142,6 +144,16 @@ impl PointerRouter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PointerEventKind::Scroll { delta } => {
|
||||||
|
if let Some(target) = hit_target {
|
||||||
|
routed.push(RoutedPointerEvent {
|
||||||
|
kind: RoutedPointerEventKind::Scroll { delta },
|
||||||
|
target,
|
||||||
|
pointer_id: event.pointer_id,
|
||||||
|
position: event.position,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
PointerEventKind::LeaveWindow => {}
|
PointerEventKind::LeaveWindow => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +177,11 @@ mod tests {
|
|||||||
element_id: None,
|
element_id: None,
|
||||||
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
||||||
corner_radius: 0.0,
|
corner_radius: 0.0,
|
||||||
|
clip_rect: None,
|
||||||
pointer_events: false,
|
pointer_events: false,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
|
scroll_metrics: None,
|
||||||
prepared_image: None,
|
prepared_image: None,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: vec![
|
children: vec![
|
||||||
@@ -176,9 +190,11 @@ mod tests {
|
|||||||
element_id: Some(ElementId::new(1)),
|
element_id: Some(ElementId::new(1)),
|
||||||
rect: Rect::new(0.0, 0.0, 120.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 120.0, 120.0),
|
||||||
corner_radius: 0.0,
|
corner_radius: 0.0,
|
||||||
|
clip_rect: None,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
|
scroll_metrics: None,
|
||||||
prepared_image: None,
|
prepared_image: None,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -188,9 +204,11 @@ mod tests {
|
|||||||
element_id: Some(ElementId::new(2)),
|
element_id: Some(ElementId::new(2)),
|
||||||
rect: Rect::new(80.0, 0.0, 120.0, 120.0),
|
rect: Rect::new(80.0, 0.0, 120.0, 120.0),
|
||||||
corner_radius: 0.0,
|
corner_radius: 0.0,
|
||||||
|
clip_rect: None,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
|
scroll_metrics: None,
|
||||||
prepared_image: None,
|
prepared_image: None,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -207,9 +225,11 @@ mod tests {
|
|||||||
element_id: None,
|
element_id: None,
|
||||||
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
||||||
corner_radius: 0.0,
|
corner_radius: 0.0,
|
||||||
|
clip_rect: None,
|
||||||
pointer_events: false,
|
pointer_events: false,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
|
scroll_metrics: None,
|
||||||
prepared_image: None,
|
prepared_image: None,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: vec![LayoutNode {
|
children: vec![LayoutNode {
|
||||||
@@ -217,9 +237,11 @@ mod tests {
|
|||||||
element_id: Some(ElementId::new(1)),
|
element_id: Some(ElementId::new(1)),
|
||||||
rect: Rect::new(0.0, 0.0, 160.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 160.0, 120.0),
|
||||||
corner_radius: 0.0,
|
corner_radius: 0.0,
|
||||||
|
clip_rect: None,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
|
scroll_metrics: None,
|
||||||
prepared_image: None,
|
prepared_image: None,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: vec![LayoutNode {
|
children: vec![LayoutNode {
|
||||||
@@ -227,9 +249,11 @@ mod tests {
|
|||||||
element_id: Some(ElementId::new(2)),
|
element_id: Some(ElementId::new(2)),
|
||||||
rect: Rect::new(16.0, 16.0, 80.0, 40.0),
|
rect: Rect::new(16.0, 16.0, 80.0, 40.0),
|
||||||
corner_radius: 0.0,
|
corner_radius: 0.0,
|
||||||
|
clip_rect: None,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
|
scroll_metrics: None,
|
||||||
prepared_image: None,
|
prepared_image: None,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -290,6 +314,35 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn router_routes_scroll_to_deepest_hover_target() {
|
||||||
|
let mut router = PointerRouter::new();
|
||||||
|
let tree = nested_interaction_tree();
|
||||||
|
let _ = router.route(
|
||||||
|
&tree,
|
||||||
|
PointerEvent::new(0, Point::new(24.0, 24.0), PointerEventKind::Move),
|
||||||
|
);
|
||||||
|
|
||||||
|
let routed = router.route(
|
||||||
|
&tree,
|
||||||
|
PointerEvent::new(
|
||||||
|
0,
|
||||||
|
Point::new(24.0, 24.0),
|
||||||
|
PointerEventKind::Scroll {
|
||||||
|
delta: Point::new(0.0, 18.0),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(routed.iter().any(|event| {
|
||||||
|
event.kind
|
||||||
|
== RoutedPointerEventKind::Scroll {
|
||||||
|
delta: Point::new(0.0, 18.0),
|
||||||
|
}
|
||||||
|
&& event.target.element_id == Some(ElementId::new(2))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn router_tracks_hovered_ancestors_for_nested_hits() {
|
fn router_tracks_hovered_ancestors_for_nested_hits() {
|
||||||
let mut router = PointerRouter::new();
|
let mut router = PointerRouter::new();
|
||||||
|
|||||||
@@ -61,14 +61,26 @@ pub struct LayoutNode {
|
|||||||
pub element_id: Option<ElementId>,
|
pub element_id: Option<ElementId>,
|
||||||
pub rect: Rect,
|
pub rect: Rect,
|
||||||
pub corner_radius: f32,
|
pub corner_radius: f32,
|
||||||
|
pub clip_rect: Option<Rect>,
|
||||||
pub pointer_events: bool,
|
pub pointer_events: bool,
|
||||||
pub focusable: bool,
|
pub focusable: bool,
|
||||||
pub cursor: CursorIcon,
|
pub cursor: CursorIcon,
|
||||||
|
pub scroll_metrics: Option<ScrollMetrics>,
|
||||||
pub prepared_image: Option<PreparedImage>,
|
pub prepared_image: Option<PreparedImage>,
|
||||||
pub prepared_text: Option<PreparedText>,
|
pub prepared_text: Option<PreparedText>,
|
||||||
pub children: Vec<LayoutNode>,
|
pub children: Vec<LayoutNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ScrollMetrics {
|
||||||
|
pub viewport_rect: Rect,
|
||||||
|
pub content_height: f32,
|
||||||
|
pub offset_y: f32,
|
||||||
|
pub max_offset_y: f32,
|
||||||
|
pub scrollbar_track: Option<Rect>,
|
||||||
|
pub scrollbar_thumb: Option<Rect>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct TextHitTarget {
|
pub struct TextHitTarget {
|
||||||
pub target: HitTarget,
|
pub target: HitTarget,
|
||||||
@@ -100,6 +112,10 @@ impl InteractionTree {
|
|||||||
pub fn text_for_element(&self, element_id: ElementId) -> Option<&PreparedText> {
|
pub fn text_for_element(&self, element_id: ElementId) -> Option<&PreparedText> {
|
||||||
text_for_element_node(&self.root, element_id)
|
text_for_element_node(&self.root, element_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_metrics_for_element(&self, element_id: ElementId) -> Option<&ScrollMetrics> {
|
||||||
|
scroll_metrics_for_element_node(&self.root, element_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_snapshot(version: u64, logical_size: UiSize, root: &Element) -> LayoutSnapshot {
|
pub fn layout_snapshot(version: u64, logical_size: UiSize, root: &Element) -> LayoutSnapshot {
|
||||||
@@ -191,9 +207,11 @@ fn layout_element(
|
|||||||
element_id: element.id,
|
element_id: element.id,
|
||||||
rect,
|
rect,
|
||||||
corner_radius: uniform_corner_radius(&element.style, rect),
|
corner_radius: uniform_corner_radius(&element.style, rect),
|
||||||
|
clip_rect: None,
|
||||||
pointer_events: element.style.pointer_events,
|
pointer_events: element.style.pointer_events,
|
||||||
focusable: element.style.focusable,
|
focusable: element.style.focusable,
|
||||||
cursor,
|
cursor,
|
||||||
|
scroll_metrics: None,
|
||||||
prepared_image: None,
|
prepared_image: None,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -264,6 +282,98 @@ fn layout_element(
|
|||||||
return interaction;
|
return interaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(scroll_box) = element.scroll_box_node() {
|
||||||
|
perf_stats.container_nodes += 1;
|
||||||
|
let content = inset_rect(rect, content_insets(&element.style));
|
||||||
|
if content.size.width > 0.0 && content.size.height > 0.0 {
|
||||||
|
let gutter_width = scroll_box
|
||||||
|
.scrollbar
|
||||||
|
.gutter_width
|
||||||
|
.max(0.0)
|
||||||
|
.min(content.size.width);
|
||||||
|
let viewport_rect = Rect::new(
|
||||||
|
content.origin.x,
|
||||||
|
content.origin.y,
|
||||||
|
(content.size.width - gutter_width).max(0.0),
|
||||||
|
content.size.height,
|
||||||
|
);
|
||||||
|
interaction.clip_rect = Some(viewport_rect);
|
||||||
|
|
||||||
|
let content_size = intrinsic_container_content_size(
|
||||||
|
element,
|
||||||
|
UiSize::new(
|
||||||
|
viewport_rect.size.width.max(0.0),
|
||||||
|
viewport_rect.size.height.max(0.0),
|
||||||
|
),
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
);
|
||||||
|
let provisional_content_height = content_size.height.max(viewport_rect.size.height);
|
||||||
|
let mut offset_y = scroll_box.offset_y.max(0.0);
|
||||||
|
|
||||||
|
if viewport_rect.size.width > 0.0 && viewport_rect.size.height > 0.0 {
|
||||||
|
scene.push_clip(viewport_rect, 0.0);
|
||||||
|
interaction.children = layout_container_children(
|
||||||
|
element,
|
||||||
|
Rect::new(
|
||||||
|
viewport_rect.origin.x,
|
||||||
|
viewport_rect.origin.y - offset_y,
|
||||||
|
viewport_rect.size.width,
|
||||||
|
provisional_content_height,
|
||||||
|
),
|
||||||
|
&interaction.path,
|
||||||
|
scene,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
);
|
||||||
|
scene.pop_clip();
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual_content_height = interaction
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.filter_map(max_layout_node_bottom)
|
||||||
|
.fold(viewport_rect.origin.y - offset_y, f32::max)
|
||||||
|
- (viewport_rect.origin.y - offset_y);
|
||||||
|
let content_height = provisional_content_height
|
||||||
|
.max(actual_content_height.ceil())
|
||||||
|
.max(viewport_rect.size.height);
|
||||||
|
let max_offset_y = (content_height - viewport_rect.size.height).max(0.0);
|
||||||
|
offset_y = offset_y.min(max_offset_y);
|
||||||
|
interaction.scroll_metrics = Some(ScrollMetrics {
|
||||||
|
viewport_rect,
|
||||||
|
content_height,
|
||||||
|
offset_y,
|
||||||
|
max_offset_y,
|
||||||
|
scrollbar_track: None,
|
||||||
|
scrollbar_thumb: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let (scrollbar_track, scrollbar_thumb) = scrollbar_geometry(
|
||||||
|
content,
|
||||||
|
scroll_box.scrollbar,
|
||||||
|
content_height,
|
||||||
|
viewport_rect.size.height,
|
||||||
|
offset_y,
|
||||||
|
);
|
||||||
|
if let Some(scroll_metrics) = interaction.scroll_metrics.as_mut() {
|
||||||
|
scroll_metrics.scrollbar_track = scrollbar_track;
|
||||||
|
scroll_metrics.scrollbar_thumb = scrollbar_thumb;
|
||||||
|
}
|
||||||
|
paint_scrollbar(
|
||||||
|
scene,
|
||||||
|
scroll_box.scrollbar,
|
||||||
|
scrollbar_track,
|
||||||
|
scrollbar_thumb,
|
||||||
|
perf_stats,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if pushed_clip {
|
||||||
|
scene.pop_clip();
|
||||||
|
}
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
|
||||||
perf_stats.container_nodes += 1;
|
perf_stats.container_nodes += 1;
|
||||||
|
|
||||||
if element.children.is_empty() {
|
if element.children.is_empty() {
|
||||||
@@ -274,90 +384,14 @@ fn layout_element(
|
|||||||
if content.size.width <= 0.0 || content.size.height <= 0.0 {
|
if content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||||
return interaction;
|
return interaction;
|
||||||
}
|
}
|
||||||
|
interaction.children = layout_container_children(
|
||||||
let gap_count = element.children.len().saturating_sub(1) as f32;
|
element,
|
||||||
let total_gap = element.style.gap * gap_count;
|
content,
|
||||||
let available_main = main_axis_size(content.size, element.style.direction).max(0.0) - total_gap;
|
&interaction.path,
|
||||||
let available_main = available_main.max(0.0);
|
scene,
|
||||||
let available_cross = cross_axis_size(content.size, element.style.direction).max(0.0);
|
text_system,
|
||||||
|
perf_stats,
|
||||||
let mut measured_children = Vec::with_capacity(element.children.len());
|
);
|
||||||
let mut fixed_total = 0.0;
|
|
||||||
let mut flex_total = 0.0;
|
|
||||||
for child in &element.children {
|
|
||||||
let cross = child_cross_size(child, element.style.direction)
|
|
||||||
.unwrap_or(available_cross)
|
|
||||||
.clamp(0.0, available_cross);
|
|
||||||
let explicit_main =
|
|
||||||
child_main_size(child, element.style.direction).map(|main| main.max(0.0));
|
|
||||||
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
|
||||||
let measured_main = explicit_main.unwrap_or_else(|| {
|
|
||||||
if is_flex {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
perf_stats.intrinsic_calls += 1;
|
|
||||||
if child.text_node().is_some() {
|
|
||||||
perf_stats.intrinsic_text_calls += 1;
|
|
||||||
} else {
|
|
||||||
perf_stats.intrinsic_container_calls += 1;
|
|
||||||
}
|
|
||||||
let intrinsic_started = perf_stats.enabled.then(Instant::now);
|
|
||||||
let intrinsic = intrinsic_main_size(
|
|
||||||
child,
|
|
||||||
element.style.direction,
|
|
||||||
cross,
|
|
||||||
available_main,
|
|
||||||
text_system,
|
|
||||||
perf_stats,
|
|
||||||
);
|
|
||||||
if let Some(intrinsic_started) = intrinsic_started {
|
|
||||||
perf_stats.intrinsic_ms += intrinsic_started.elapsed().as_secs_f64() * 1_000.0;
|
|
||||||
}
|
|
||||||
intrinsic
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if is_flex {
|
|
||||||
flex_total += child_flex_weight(child);
|
|
||||||
} else {
|
|
||||||
fixed_total += measured_main;
|
|
||||||
}
|
|
||||||
measured_children.push(MeasuredChild {
|
|
||||||
cross,
|
|
||||||
main: measured_main,
|
|
||||||
is_flex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining_main = (available_main - fixed_total).max(0.0);
|
|
||||||
let mut cursor = main_axis_origin(content, element.style.direction);
|
|
||||||
|
|
||||||
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
|
||||||
let child_main = if measured.is_flex {
|
|
||||||
if flex_total <= 0.0 {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
remaining_main * (child_flex_weight(child) / flex_total)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
measured.main
|
|
||||||
};
|
|
||||||
let child_rect = child_rect(
|
|
||||||
content,
|
|
||||||
element.style.direction,
|
|
||||||
cursor,
|
|
||||||
child_main.max(0.0),
|
|
||||||
measured.cross,
|
|
||||||
);
|
|
||||||
interaction.children.push(layout_element(
|
|
||||||
child,
|
|
||||||
child_rect,
|
|
||||||
interaction.path.child(index),
|
|
||||||
scene,
|
|
||||||
text_system,
|
|
||||||
perf_stats,
|
|
||||||
));
|
|
||||||
cursor += child_main.max(0.0) + element.style.gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
if pushed_clip {
|
if pushed_clip {
|
||||||
scene.pop_clip();
|
scene.pop_clip();
|
||||||
@@ -371,18 +405,23 @@ fn hit_path_node(node: &LayoutNode, point: crate::scene::Point) -> Option<Vec<Hi
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
for child in node.children.iter().rev() {
|
if node
|
||||||
if let Some(mut hits) = hit_path_node(child, point) {
|
.clip_rect
|
||||||
if node.pointer_events {
|
.is_none_or(|clip_rect| clip_rect.contains(point))
|
||||||
hits.push(HitTarget {
|
{
|
||||||
path: node.path.clone(),
|
for child in node.children.iter().rev() {
|
||||||
element_id: node.element_id,
|
if let Some(mut hits) = hit_path_node(child, point) {
|
||||||
rect: node.rect,
|
if node.pointer_events {
|
||||||
focusable: node.focusable,
|
hits.push(HitTarget {
|
||||||
cursor: node.cursor,
|
path: node.path.clone(),
|
||||||
});
|
element_id: node.element_id,
|
||||||
|
rect: node.rect,
|
||||||
|
focusable: node.focusable,
|
||||||
|
cursor: node.cursor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Some(hits);
|
||||||
}
|
}
|
||||||
return Some(hits);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,9 +443,14 @@ fn text_hit_test_node(node: &LayoutNode, point: crate::scene::Point) -> Option<T
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
for child in node.children.iter().rev() {
|
if node
|
||||||
if let Some(hit) = text_hit_test_node(child, point) {
|
.clip_rect
|
||||||
return Some(hit);
|
.is_none_or(|clip_rect| clip_rect.contains(point))
|
||||||
|
{
|
||||||
|
for child in node.children.iter().rev() {
|
||||||
|
if let Some(hit) = text_hit_test_node(child, point) {
|
||||||
|
return Some(hit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +490,25 @@ fn text_for_element_node(node: &LayoutNode, element_id: ElementId) -> Option<&Pr
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scroll_metrics_for_element_node(
|
||||||
|
node: &LayoutNode,
|
||||||
|
element_id: ElementId,
|
||||||
|
) -> Option<&ScrollMetrics> {
|
||||||
|
if node.element_id == Some(element_id)
|
||||||
|
&& let Some(scroll_metrics) = node.scroll_metrics.as_ref()
|
||||||
|
{
|
||||||
|
return Some(scroll_metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in &node.children {
|
||||||
|
if let Some(scroll_metrics) = scroll_metrics_for_element_node(child, element_id) {
|
||||||
|
return Some(scroll_metrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn point_hits_node_shape(node: &LayoutNode, point: crate::scene::Point) -> bool {
|
fn point_hits_node_shape(node: &LayoutNode, point: crate::scene::Point) -> bool {
|
||||||
node.rect.contains(point)
|
node.rect.contains(point)
|
||||||
&& (node.corner_radius <= 0.0
|
&& (node.corner_radius <= 0.0
|
||||||
@@ -496,6 +559,104 @@ struct MeasuredChild {
|
|||||||
is_flex: bool,
|
is_flex: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout_container_children(
|
||||||
|
element: &Element,
|
||||||
|
content: Rect,
|
||||||
|
path: &LayoutPath,
|
||||||
|
scene: &mut SceneSnapshot,
|
||||||
|
text_system: &mut TextSystem,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
) -> Vec<LayoutNode> {
|
||||||
|
if element.children.is_empty() || content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let gap_count = element.children.len().saturating_sub(1) as f32;
|
||||||
|
let total_gap = element.style.gap * gap_count;
|
||||||
|
let available_main =
|
||||||
|
(main_axis_size(content.size, element.style.direction).max(0.0) - total_gap).max(0.0);
|
||||||
|
let available_cross = cross_axis_size(content.size, element.style.direction).max(0.0);
|
||||||
|
|
||||||
|
let mut measured_children = Vec::with_capacity(element.children.len());
|
||||||
|
let mut fixed_total = 0.0;
|
||||||
|
let mut flex_total = 0.0;
|
||||||
|
for child in &element.children {
|
||||||
|
let cross = child_cross_size(child, element.style.direction)
|
||||||
|
.unwrap_or(available_cross)
|
||||||
|
.clamp(0.0, available_cross);
|
||||||
|
let explicit_main =
|
||||||
|
child_main_size(child, element.style.direction).map(|main| main.max(0.0));
|
||||||
|
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
||||||
|
let measured_main = explicit_main.unwrap_or_else(|| {
|
||||||
|
if is_flex {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
perf_stats.intrinsic_calls += 1;
|
||||||
|
if child.text_node().is_some() {
|
||||||
|
perf_stats.intrinsic_text_calls += 1;
|
||||||
|
} else {
|
||||||
|
perf_stats.intrinsic_container_calls += 1;
|
||||||
|
}
|
||||||
|
let intrinsic_started = perf_stats.enabled.then(Instant::now);
|
||||||
|
let intrinsic = intrinsic_main_size(
|
||||||
|
child,
|
||||||
|
element.style.direction,
|
||||||
|
cross,
|
||||||
|
available_main,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
);
|
||||||
|
if let Some(intrinsic_started) = intrinsic_started {
|
||||||
|
perf_stats.intrinsic_ms += intrinsic_started.elapsed().as_secs_f64() * 1_000.0;
|
||||||
|
}
|
||||||
|
intrinsic
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if is_flex {
|
||||||
|
flex_total += child_flex_weight(child);
|
||||||
|
} else {
|
||||||
|
fixed_total += measured_main;
|
||||||
|
}
|
||||||
|
measured_children.push(MeasuredChild {
|
||||||
|
cross,
|
||||||
|
main: measured_main,
|
||||||
|
is_flex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining_main = (available_main - fixed_total).max(0.0);
|
||||||
|
let mut cursor = main_axis_origin(content, element.style.direction);
|
||||||
|
let mut children = Vec::with_capacity(element.children.len());
|
||||||
|
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||||
|
let child_main = if measured.is_flex {
|
||||||
|
if flex_total <= 0.0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
remaining_main * (child_flex_weight(child) / flex_total)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
measured.main
|
||||||
|
};
|
||||||
|
let child_rect = child_rect(
|
||||||
|
content,
|
||||||
|
element.style.direction,
|
||||||
|
cursor,
|
||||||
|
child_main.max(0.0),
|
||||||
|
measured.cross,
|
||||||
|
);
|
||||||
|
children.push(layout_element(
|
||||||
|
child,
|
||||||
|
child_rect,
|
||||||
|
path.child(index),
|
||||||
|
scene,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
));
|
||||||
|
cursor += child_main.max(0.0) + element.style.gap;
|
||||||
|
}
|
||||||
|
children
|
||||||
|
}
|
||||||
|
|
||||||
fn intrinsic_main_size(
|
fn intrinsic_main_size(
|
||||||
child: &Element,
|
child: &Element,
|
||||||
direction: FlexDirection,
|
direction: FlexDirection,
|
||||||
@@ -530,58 +691,18 @@ fn intrinsic_main_size(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn intrinsic_size(
|
fn intrinsic_container_content_size(
|
||||||
element: &Element,
|
element: &Element,
|
||||||
available_size: UiSize,
|
content_size: UiSize,
|
||||||
text_system: &mut TextSystem,
|
text_system: &mut TextSystem,
|
||||||
perf_stats: &mut LayoutPerfStats,
|
perf_stats: &mut LayoutPerfStats,
|
||||||
) -> UiSize {
|
) -> UiSize {
|
||||||
perf_stats.intrinsic_size_calls += 1;
|
|
||||||
let insets = content_insets(&element.style);
|
|
||||||
if let Some(text) = element.text_node() {
|
|
||||||
let measured = text_system.measure_spans(
|
|
||||||
&text.spans,
|
|
||||||
&text.style,
|
|
||||||
Some(available_size.width.max(0.0)),
|
|
||||||
Some(available_size.height.max(0.0)),
|
|
||||||
);
|
|
||||||
return UiSize::new(
|
|
||||||
element
|
|
||||||
.style
|
|
||||||
.width
|
|
||||||
.unwrap_or(measured.width + horizontal_insets(insets)),
|
|
||||||
element
|
|
||||||
.style
|
|
||||||
.height
|
|
||||||
.unwrap_or(measured.height + vertical_insets(insets)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(image) = element.image_node() {
|
|
||||||
let resolved = resolve_image_element_size(element, image.resource.size());
|
|
||||||
return UiSize::new(
|
|
||||||
resolved.width + horizontal_insets(insets),
|
|
||||||
resolved.height + vertical_insets(insets),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let explicit_width = element.style.width;
|
|
||||||
let explicit_height = element.style.height;
|
|
||||||
let content_width =
|
|
||||||
explicit_width.unwrap_or(available_size.width).max(0.0) - horizontal_insets(insets);
|
|
||||||
let content_height =
|
|
||||||
explicit_height.unwrap_or(available_size.height).max(0.0) - vertical_insets(insets);
|
|
||||||
let content_size = UiSize::new(content_width.max(0.0), content_height.max(0.0));
|
|
||||||
|
|
||||||
if element.children.is_empty() {
|
if element.children.is_empty() {
|
||||||
return UiSize::new(
|
return UiSize::new(0.0, 0.0);
|
||||||
explicit_width.unwrap_or(horizontal_insets(insets)),
|
|
||||||
explicit_height.unwrap_or(vertical_insets(insets)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let gap_total = element.style.gap * element.children.len().saturating_sub(1) as f32;
|
let gap_total = element.style.gap * element.children.len().saturating_sub(1) as f32;
|
||||||
let (intrinsic_content_width, intrinsic_content_height) = match element.style.direction {
|
match element.style.direction {
|
||||||
FlexDirection::Column => {
|
FlexDirection::Column => {
|
||||||
let mut width: f32 = 0.0;
|
let mut width: f32 = 0.0;
|
||||||
let mut height = gap_total;
|
let mut height = gap_total;
|
||||||
@@ -601,7 +722,7 @@ fn intrinsic_size(
|
|||||||
height += child.style.height.unwrap_or(child_size.height);
|
height += child.style.height.unwrap_or(child_size.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(width, height)
|
UiSize::new(width, height)
|
||||||
}
|
}
|
||||||
FlexDirection::Row => {
|
FlexDirection::Row => {
|
||||||
let mut width = gap_total;
|
let mut width = gap_total;
|
||||||
@@ -653,13 +774,74 @@ fn intrinsic_size(
|
|||||||
}
|
}
|
||||||
height = height.max(child.style.height.unwrap_or(child_size.height));
|
height = height.max(child.style.height.unwrap_or(child_size.height));
|
||||||
}
|
}
|
||||||
(width, height)
|
UiSize::new(width, height)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intrinsic_size(
|
||||||
|
element: &Element,
|
||||||
|
available_size: UiSize,
|
||||||
|
text_system: &mut TextSystem,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
) -> UiSize {
|
||||||
|
perf_stats.intrinsic_size_calls += 1;
|
||||||
|
let insets = content_insets(&element.style);
|
||||||
|
if let Some(text) = element.text_node() {
|
||||||
|
let measured = text_system.measure_spans(
|
||||||
|
&text.spans,
|
||||||
|
&text.style,
|
||||||
|
Some(available_size.width.max(0.0)),
|
||||||
|
Some(available_size.height.max(0.0)),
|
||||||
|
);
|
||||||
|
return UiSize::new(
|
||||||
|
element
|
||||||
|
.style
|
||||||
|
.width
|
||||||
|
.unwrap_or(measured.width + horizontal_insets(insets)),
|
||||||
|
element
|
||||||
|
.style
|
||||||
|
.height
|
||||||
|
.unwrap_or(measured.height + vertical_insets(insets)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(image) = element.image_node() {
|
||||||
|
let resolved = resolve_image_element_size(element, image.resource.size());
|
||||||
|
return UiSize::new(
|
||||||
|
resolved.width + horizontal_insets(insets),
|
||||||
|
resolved.height + vertical_insets(insets),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let explicit_width = element.style.width;
|
||||||
|
let explicit_height = element.style.height;
|
||||||
|
let scroll_gutter = element
|
||||||
|
.scroll_box_node()
|
||||||
|
.map_or(0.0, |scroll_box| scroll_box.scrollbar.gutter_width.max(0.0));
|
||||||
|
let content_width =
|
||||||
|
explicit_width.unwrap_or(available_size.width).max(0.0) - horizontal_insets(insets);
|
||||||
|
let content_height =
|
||||||
|
explicit_height.unwrap_or(available_size.height).max(0.0) - vertical_insets(insets);
|
||||||
|
let content_size = UiSize::new(
|
||||||
|
(content_width - scroll_gutter).max(0.0),
|
||||||
|
content_height.max(0.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
if element.children.is_empty() {
|
||||||
|
return UiSize::new(
|
||||||
|
explicit_width.unwrap_or(horizontal_insets(insets) + scroll_gutter),
|
||||||
|
explicit_height.unwrap_or(vertical_insets(insets)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let intrinsic_content =
|
||||||
|
intrinsic_container_content_size(element, content_size, text_system, perf_stats);
|
||||||
|
|
||||||
UiSize::new(
|
UiSize::new(
|
||||||
explicit_width.unwrap_or(intrinsic_content_width + horizontal_insets(insets)),
|
explicit_width
|
||||||
explicit_height.unwrap_or(intrinsic_content_height + vertical_insets(insets)),
|
.unwrap_or(intrinsic_content.width + horizontal_insets(insets) + scroll_gutter),
|
||||||
|
explicit_height.unwrap_or(intrinsic_content.height + vertical_insets(insets)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,6 +941,116 @@ fn resolve_image_element_size(element: &Element, intrinsic: UiSize) -> UiSize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scrollbar_geometry(
|
||||||
|
content_rect: Rect,
|
||||||
|
scrollbar: crate::ScrollbarStyle,
|
||||||
|
content_height: f32,
|
||||||
|
viewport_height: f32,
|
||||||
|
offset_y: f32,
|
||||||
|
) -> (Option<Rect>, Option<Rect>) {
|
||||||
|
let gutter_width = scrollbar.gutter_width.max(0.0).min(content_rect.size.width);
|
||||||
|
if gutter_width <= 0.0 || content_rect.size.height <= 0.0 {
|
||||||
|
return (None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let gutter_rect = Rect::new(
|
||||||
|
content_rect.origin.x + content_rect.size.width - gutter_width,
|
||||||
|
content_rect.origin.y,
|
||||||
|
gutter_width,
|
||||||
|
content_rect.size.height,
|
||||||
|
);
|
||||||
|
let track_rect = inset_rect(
|
||||||
|
gutter_rect,
|
||||||
|
Edges {
|
||||||
|
top: 0.0,
|
||||||
|
right: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
left: (gutter_width * 0.16).clamp(1.0, gutter_width * 0.5),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if track_rect.size.width <= 0.0 || track_rect.size.height <= 0.0 {
|
||||||
|
return (None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_offset_y = (content_height - viewport_height).max(0.0);
|
||||||
|
if max_offset_y <= 0.0 {
|
||||||
|
return (Some(track_rect), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let thumb_height = ((viewport_height / content_height.max(viewport_height))
|
||||||
|
* track_rect.size.height)
|
||||||
|
.max(scrollbar.min_thumb_size.max(0.0))
|
||||||
|
.min(track_rect.size.height);
|
||||||
|
let thumb_range = (track_rect.size.height - thumb_height).max(0.0);
|
||||||
|
let thumb_origin_y = track_rect.origin.y + thumb_range * (offset_y / max_offset_y.max(1.0));
|
||||||
|
let thumb_rect = Rect::new(
|
||||||
|
track_rect.origin.x,
|
||||||
|
thumb_origin_y,
|
||||||
|
track_rect.size.width,
|
||||||
|
thumb_height,
|
||||||
|
);
|
||||||
|
(Some(track_rect), Some(thumb_rect))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_scrollbar(
|
||||||
|
scene: &mut SceneSnapshot,
|
||||||
|
scrollbar: crate::ScrollbarStyle,
|
||||||
|
track_rect: Option<Rect>,
|
||||||
|
thumb_rect: Option<Rect>,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
) {
|
||||||
|
let radius_for = |rect: Rect| {
|
||||||
|
scrollbar
|
||||||
|
.corner_radius
|
||||||
|
.max(0.0)
|
||||||
|
.min(rect.size.width * 0.5)
|
||||||
|
.min(rect.size.height * 0.5)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(track_rect) = track_rect {
|
||||||
|
perf_stats.background_quads += 1;
|
||||||
|
scene.push_rounded_rect(RoundedRect {
|
||||||
|
rect: track_rect,
|
||||||
|
fill: Some(scrollbar.track_color),
|
||||||
|
border_color: None,
|
||||||
|
border_width: 0.0,
|
||||||
|
radius: radius_for(track_rect),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(thumb_rect) = thumb_rect {
|
||||||
|
perf_stats.background_quads += 1;
|
||||||
|
scene.push_rounded_rect(RoundedRect {
|
||||||
|
rect: thumb_rect,
|
||||||
|
fill: Some(scrollbar.thumb_color),
|
||||||
|
border_color: None,
|
||||||
|
border_width: 0.0,
|
||||||
|
radius: radius_for(thumb_rect),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_layout_node_bottom(node: &LayoutNode) -> Option<f32> {
|
||||||
|
let mut max_bottom =
|
||||||
|
(node.rect.size.height > 0.0).then_some(node.rect.origin.y + node.rect.size.height);
|
||||||
|
if let Some(prepared_text) = node.prepared_text.as_ref()
|
||||||
|
&& let Some(bounds) = prepared_text.bounds
|
||||||
|
{
|
||||||
|
let text_bottom = prepared_text.origin.y + bounds.height;
|
||||||
|
max_bottom = Some(max_bottom.map_or(text_bottom, |current| current.max(text_bottom)));
|
||||||
|
}
|
||||||
|
if let Some(prepared_image) = node.prepared_image.as_ref() {
|
||||||
|
let image_bottom = prepared_image.rect.origin.y + prepared_image.rect.size.height;
|
||||||
|
max_bottom = Some(max_bottom.map_or(image_bottom, |current| current.max(image_bottom)));
|
||||||
|
}
|
||||||
|
for child in &node.children {
|
||||||
|
if let Some(child_bottom) = max_layout_node_bottom(child) {
|
||||||
|
max_bottom = Some(max_bottom.map_or(child_bottom, |current| current.max(child_bottom)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max_bottom
|
||||||
|
}
|
||||||
|
|
||||||
fn push_box_shadows(
|
fn push_box_shadows(
|
||||||
scene: &mut SceneSnapshot,
|
scene: &mut SceneSnapshot,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
@@ -1270,6 +1562,42 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_box_exposes_scroll_metrics_and_reserves_scrollbar_gutter() {
|
||||||
|
let scrollbox_id = ElementId::new(9);
|
||||||
|
let root = Element::column().child(
|
||||||
|
Element::scroll_box(48.0)
|
||||||
|
.id(scrollbox_id)
|
||||||
|
.width(180.0)
|
||||||
|
.height(96.0)
|
||||||
|
.padding(Edges::all(8.0))
|
||||||
|
.children([
|
||||||
|
Element::paragraph(
|
||||||
|
"Scroll boxes should clip oversized content and reserve space for a scrollbar.",
|
||||||
|
TextStyle::new(16.0, Color::rgb(0xFF, 0xFF, 0xFF)).with_line_height(22.0),
|
||||||
|
),
|
||||||
|
Element::paragraph(
|
||||||
|
"This extra paragraph forces overflow so the layout exposes max offset and scrollbar geometry.",
|
||||||
|
TextStyle::new(16.0, Color::rgb(0xFF, 0xFF, 0xFF)).with_line_height(22.0),
|
||||||
|
),
|
||||||
|
Element::paragraph(
|
||||||
|
"Keyboard and wheel handling use these metrics to clamp scrolling in the demo.",
|
||||||
|
TextStyle::new(16.0, Color::rgb(0xFF, 0xFF, 0xFF)).with_line_height(22.0),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = layout_snapshot(1, UiSize::new(240.0, 220.0), &root);
|
||||||
|
let scroll_metrics = snapshot
|
||||||
|
.interaction_tree
|
||||||
|
.scroll_metrics_for_element(scrollbox_id)
|
||||||
|
.expect("scroll box should expose scroll metrics");
|
||||||
|
assert!(scroll_metrics.max_offset_y > 0.0);
|
||||||
|
assert!(scroll_metrics.viewport_rect.size.width < 180.0);
|
||||||
|
assert!(scroll_metrics.scrollbar_track.is_some());
|
||||||
|
assert!(scroll_metrics.scrollbar_thumb.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interaction_tree_hit_test_returns_deepest_pointer_target() {
|
fn interaction_tree_hit_test_returns_deepest_pointer_target() {
|
||||||
let root = Element::column()
|
let root = Element::column()
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ pub use interaction::{
|
|||||||
};
|
};
|
||||||
pub use keyboard::{KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers};
|
pub use keyboard::{KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers};
|
||||||
pub use layout::{
|
pub use layout::{
|
||||||
HitTarget, InteractionTree, LayoutNode, LayoutPath, LayoutSnapshot, TextHitTarget,
|
HitTarget, InteractionTree, LayoutNode, LayoutPath, LayoutSnapshot, ScrollMetrics,
|
||||||
layout_snapshot, layout_snapshot_with_text_system,
|
TextHitTarget, layout_snapshot, layout_snapshot_with_text_system,
|
||||||
};
|
};
|
||||||
pub use layout::{layout_scene, layout_scene_with_text_system};
|
pub use layout::{layout_scene, layout_scene_with_text_system};
|
||||||
pub use platform::{
|
pub use platform::{
|
||||||
@@ -47,7 +47,7 @@ pub use text::{
|
|||||||
};
|
};
|
||||||
pub use tree::{
|
pub use tree::{
|
||||||
Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element, ElementId,
|
Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element, ElementId,
|
||||||
FlexDirection, Style,
|
FlexDirection, ScrollbarStyle, Style,
|
||||||
};
|
};
|
||||||
pub use window::{
|
pub use window::{
|
||||||
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
||||||
|
|||||||
@@ -134,6 +134,47 @@ impl BoxShadow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ScrollbarStyle {
|
||||||
|
pub gutter_width: f32,
|
||||||
|
pub track_color: Color,
|
||||||
|
pub thumb_color: Color,
|
||||||
|
pub corner_radius: f32,
|
||||||
|
pub min_thumb_size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollbarStyle {
|
||||||
|
pub const fn new(gutter_width: f32, track_color: Color, thumb_color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
gutter_width,
|
||||||
|
track_color,
|
||||||
|
thumb_color,
|
||||||
|
corner_radius: 999.0,
|
||||||
|
min_thumb_size: 28.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn with_corner_radius(mut self, corner_radius: f32) -> Self {
|
||||||
|
self.corner_radius = corner_radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn with_min_thumb_size(mut self, min_thumb_size: f32) -> Self {
|
||||||
|
self.min_thumb_size = min_thumb_size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScrollbarStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(
|
||||||
|
14.0,
|
||||||
|
Color::rgba(0xFF, 0xFF, 0xFF, 0x12),
|
||||||
|
Color::rgba(0xD8, 0xDE, 0xEA, 0x88),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Style {
|
pub struct Style {
|
||||||
pub direction: FlexDirection,
|
pub direction: FlexDirection,
|
||||||
@@ -174,6 +215,7 @@ impl Default for Style {
|
|||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
enum ElementContent {
|
enum ElementContent {
|
||||||
Container,
|
Container,
|
||||||
|
ScrollBox(ScrollBoxNode),
|
||||||
Image(ImageNode),
|
Image(ImageNode),
|
||||||
Text(TextNode),
|
Text(TextNode),
|
||||||
}
|
}
|
||||||
@@ -190,6 +232,12 @@ pub(crate) struct ImageNode {
|
|||||||
pub fit: ImageFit,
|
pub fit: ImageFit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct ScrollBoxNode {
|
||||||
|
pub offset_y: f32,
|
||||||
|
pub scrollbar: ScrollbarStyle,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Element {
|
pub struct Element {
|
||||||
pub id: Option<ElementId>,
|
pub id: Option<ElementId>,
|
||||||
@@ -240,6 +288,21 @@ impl Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_box(offset_y: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
id: None,
|
||||||
|
style: Style {
|
||||||
|
focusable: true,
|
||||||
|
..Style::default()
|
||||||
|
},
|
||||||
|
children: Vec::new(),
|
||||||
|
content: ElementContent::ScrollBox(ScrollBoxNode {
|
||||||
|
offset_y: offset_y.max(0.0),
|
||||||
|
scrollbar: ScrollbarStyle::default(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rich_paragraph(spans: impl IntoIterator<Item = TextSpan>, style: TextStyle) -> Self {
|
pub fn rich_paragraph(spans: impl IntoIterator<Item = TextSpan>, style: TextStyle) -> Self {
|
||||||
Self::spans(spans, style.with_wrap(TextWrap::Word))
|
Self::spans(spans, style.with_wrap(TextWrap::Word))
|
||||||
}
|
}
|
||||||
@@ -330,6 +393,22 @@ impl Element {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_offset(mut self, offset_y: f32) -> Self {
|
||||||
|
let ElementContent::ScrollBox(scroll_box) = &mut self.content else {
|
||||||
|
panic!("only scroll box elements can set scroll_offset");
|
||||||
|
};
|
||||||
|
scroll_box.offset_y = offset_y.max(0.0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scrollbar_style(mut self, scrollbar: ScrollbarStyle) -> Self {
|
||||||
|
let ElementContent::ScrollBox(scroll_box) = &mut self.content else {
|
||||||
|
panic!("only scroll box elements can set scrollbar_style");
|
||||||
|
};
|
||||||
|
scroll_box.scrollbar = scrollbar;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn child(mut self, child: Element) -> Self {
|
pub fn child(mut self, child: Element) -> Self {
|
||||||
self.assert_container();
|
self.assert_container();
|
||||||
self.children.push(child);
|
self.children.push(child);
|
||||||
@@ -345,20 +424,34 @@ impl Element {
|
|||||||
pub(crate) fn text_node(&self) -> Option<&TextNode> {
|
pub(crate) fn text_node(&self) -> Option<&TextNode> {
|
||||||
match &self.content {
|
match &self.content {
|
||||||
ElementContent::Text(text) => Some(text),
|
ElementContent::Text(text) => Some(text),
|
||||||
ElementContent::Container | ElementContent::Image(_) => None,
|
ElementContent::Container | ElementContent::ScrollBox(_) | ElementContent::Image(_) => {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn image_node(&self) -> Option<&ImageNode> {
|
pub(crate) fn image_node(&self) -> Option<&ImageNode> {
|
||||||
match &self.content {
|
match &self.content {
|
||||||
ElementContent::Image(image) => Some(image),
|
ElementContent::Image(image) => Some(image),
|
||||||
ElementContent::Container | ElementContent::Text(_) => None,
|
ElementContent::Container | ElementContent::ScrollBox(_) | ElementContent::Text(_) => {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn scroll_box_node(&self) -> Option<&ScrollBoxNode> {
|
||||||
|
match &self.content {
|
||||||
|
ElementContent::ScrollBox(scroll_box) => Some(scroll_box),
|
||||||
|
ElementContent::Container | ElementContent::Image(_) | ElementContent::Text(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_container(&self) {
|
fn assert_container(&self) {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(self.content, ElementContent::Container),
|
matches!(
|
||||||
|
self.content,
|
||||||
|
ElementContent::Container | ElementContent::ScrollBox(_)
|
||||||
|
),
|
||||||
"non-container elements cannot contain children"
|
"non-container elements cannot contain children"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1556,6 +1556,23 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
|||||||
.pending_pointer_events
|
.pending_pointer_events
|
||||||
.push(PointerEvent::new(0, position, kind));
|
.push(PointerEvent::new(0, position, kind));
|
||||||
}
|
}
|
||||||
|
wl_pointer::Event::Axis { axis, value, .. } => {
|
||||||
|
let Some(position) = state.pointer_position else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let delta = match axis {
|
||||||
|
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Point::new(0.0, value as f32),
|
||||||
|
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => {
|
||||||
|
Point::new(value as f32, 0.0)
|
||||||
|
}
|
||||||
|
WEnum::Value(_) | WEnum::Unknown(_) => return,
|
||||||
|
};
|
||||||
|
state.pending_pointer_events.push(PointerEvent::new(
|
||||||
|
0,
|
||||||
|
position,
|
||||||
|
PointerEventKind::Scroll { delta },
|
||||||
|
));
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user