use taffy::geometry::Point;
use taffy::prelude::*;
use winit::event::WindowEvent;
use crate::framework::{DrawContext, EventContext, Widget};
use crate::renderer::Renderer;
const SCROLLBAR_WIDTH: f32 = 8.0;
const SCROLLBAR_MARGIN: f32 = 2.0;
const SCROLLBAR_MIN_THUMB: f32 = 20.0;
pub struct WidgetNode {
pub(crate) widget: Box<dyn Widget>,
pub(crate) children: Vec<WidgetNode>,
pub(crate) node: Option<NodeId>,
pub(crate) scroll_y: f32,
pub(crate) scrollbar_dragging: bool,
pub(crate) scrollbar_drag_start_y: f32,
pub(crate) scrollbar_drag_start_scroll: f32,
}
impl WidgetNode {
pub fn new(widget: impl Widget + 'static, children: Vec<WidgetNode>) -> Self {
Self {
widget: Box::new(widget),
children,
node: None,
scroll_y: 0.0,
scrollbar_dragging: false,
scrollbar_drag_start_y: 0.0,
scrollbar_drag_start_scroll: 0.0,
}
}
}
pub fn build_taffy(node: &mut WidgetNode, taffy: &mut TaffyTree) -> NodeId {
let child_nodes = node
.children
.iter_mut()
.map(|child| build_taffy(child, taffy))
.collect::<Vec<_>>();
let style = node.widget.style();
let node_id = if child_nodes.is_empty() {
taffy.new_leaf(style).expect("create leaf")
} else {
taffy
.new_with_children(style, &child_nodes)
.expect("create node")
};
node.node = Some(node_id);
node_id
}
pub fn sync_styles(node: &mut WidgetNode, taffy: &mut TaffyTree, width: f32, height: f32, is_root: bool) {
let Some(node_id) = node.node else {
return;
};
let mut style = node.widget.style();
if is_root {
style.size = Size {
width: Dimension::Length(width),
height: Dimension::Length(height),
};
}
taffy.set_style(node_id, style).expect("set style");
for child in &mut node.children {
sync_styles(child, taffy, width, height, false);
}
}
pub fn collect_focus_paths(node: &WidgetNode, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
if node.widget.is_focusable() {
out.push(path.clone());
}
for (index, child) in node.children.iter().enumerate() {
path.push(index);
collect_focus_paths(child, path, out);
path.pop();
}
}
pub fn widget_mut_at_path<'a>(node: &'a mut WidgetNode, path: &[usize]) -> Option<&'a mut dyn Widget> {
if path.is_empty() {
return Some(node.widget.as_mut());
}
let idx = path[0];
if idx >= node.children.len() {
return None;
}
widget_mut_at_path(&mut node.children[idx], &path[1..])
}
pub fn draw_widgets(node: &WidgetNode, taffy: &TaffyTree, renderer: &mut Renderer) {
draw_widgets_offset(node, taffy, renderer, 0.0, 0.0);
}
fn draw_widgets_offset(node: &WidgetNode, taffy: &TaffyTree, renderer: &mut Renderer, parent_x: f32, parent_y: f32) {
let Some(node_id) = node.node else {
return;
};
let layout = taffy.layout(node_id).expect("layout");
let abs_x = parent_x + layout.location.x;
let abs_y = parent_y + layout.location.y;
let mut absolute_layout = *layout;
absolute_layout.location = Point { x: abs_x, y: abs_y };
let mut ctx = DrawContext {
renderer,
layout: &absolute_layout,
};
node.widget.draw(&mut ctx);
let is_scroll = node.widget.is_scrollable();
if is_scroll {
renderer.push_clip((abs_x, abs_y, layout.size.width, layout.size.height));
}
let child_y = abs_y - node.scroll_y;
for child in &node.children {
draw_widgets_offset(child, taffy, renderer, abs_x, child_y);
}
if is_scroll {
renderer.pop_clip();
let container_h = layout.size.height;
let content_h = content_height(node, taffy);
if content_h > container_h {
draw_scrollbar(renderer, abs_x, abs_y, layout.size.width, container_h, content_h, node.scroll_y);
}
}
}
fn content_height(node: &WidgetNode, taffy: &TaffyTree) -> f32 {
let mut h: f32 = 0.0;
for child in &node.children {
if let Some(child_id) = child.node {
let cl = taffy.layout(child_id).expect("child layout");
let bottom = cl.location.y + cl.size.height;
h = h.max(bottom);
}
}
h
}
fn draw_scrollbar(
renderer: &mut Renderer,
container_x: f32,
container_y: f32,
container_w: f32,
container_h: f32,
content_h: f32,
scroll_y: f32,
) {
let track_x = container_x + container_w - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN;
let track_y = container_y + SCROLLBAR_MARGIN;
let track_h = container_h - SCROLLBAR_MARGIN * 2.0;
renderer.fill_rect_rounded(
(track_x, track_y, SCROLLBAR_WIDTH, track_h),
[0.3, 0.3, 0.3, 0.15],
SCROLLBAR_WIDTH / 2.0,
);
let ratio = container_h / content_h;
let thumb_h = (ratio * track_h).max(SCROLLBAR_MIN_THUMB);
let max_scroll = (content_h - container_h).max(0.0);
let scroll_ratio = if max_scroll > 0.0 { scroll_y / max_scroll } else { 0.0 };
let thumb_y = track_y + scroll_ratio * (track_h - thumb_h);
renderer.fill_rect_rounded(
(track_x, thumb_y, SCROLLBAR_WIDTH, thumb_h),
[0.6, 0.6, 0.6, 0.5],
SCROLLBAR_WIDTH / 2.0,
);
}
pub fn dispatch_event(
node: &mut WidgetNode,
taffy: &TaffyTree,
event: &WindowEvent,
path: &mut Vec<usize>,
) -> Option<Vec<usize>> {
dispatch_event_offset(node, taffy, event, path, 0.0, 0.0)
}
fn dispatch_event_offset(
node: &mut WidgetNode,
taffy: &TaffyTree,
event: &WindowEvent,
path: &mut Vec<usize>,
parent_x: f32,
parent_y: f32,
) -> Option<Vec<usize>> {
let Some(node_id) = node.node else {
return None;
};
let layout = taffy.layout(node_id).expect("layout");
let abs_x = parent_x + layout.location.x;
let abs_y = parent_y + layout.location.y;
let child_y = abs_y - node.scroll_y;
for (index, child) in node.children.iter_mut().enumerate() {
path.push(index);
if let Some(found) = dispatch_event_offset(child, taffy, event, path, abs_x, child_y) {
return Some(found);
}
path.pop();
}
let mut absolute_layout = *layout;
absolute_layout.location = Point { x: abs_x, y: abs_y };
let mut ctx = EventContext {
event,
layout: &absolute_layout,
};
if node.widget.handle_event(&mut ctx) {
return Some(path.clone());
}
None
}
pub fn dispatch_scroll(node: &mut WidgetNode, delta_y: f32, cursor_x: f32, cursor_y: f32, taffy: &TaffyTree) {
if !dispatch_scroll_offset(node, delta_y, cursor_x, cursor_y, taffy, 0.0, 0.0) {
scroll_node(node, delta_y, taffy);
}
}
fn dispatch_scroll_offset(
node: &mut WidgetNode,
delta_y: f32,
cx: f32,
cy: f32,
taffy: &TaffyTree,
parent_x: f32,
parent_y: f32,
) -> bool {
let Some(node_id) = node.node else { return false; };
let layout = taffy.layout(node_id).expect("layout");
let abs_x = parent_x + layout.location.x;
let abs_y = parent_y + layout.location.y;
let inside = cx >= abs_x
&& cx <= abs_x + layout.size.width
&& cy >= abs_y
&& cy <= abs_y + layout.size.height;
if !inside {
return false;
}
let child_y = abs_y - node.scroll_y;
for child in &mut node.children {
if dispatch_scroll_offset(child, delta_y, cx, cy, taffy, abs_x, child_y) {
return true;
}
}
if node.widget.is_scrollable() {
scroll_node(node, delta_y, taffy);
return true;
}
false
}
fn scroll_node(node: &mut WidgetNode, delta_y: f32, taffy: &TaffyTree) {
let Some(node_id) = node.node else { return; };
let layout = taffy.layout(node_id).expect("layout");
let container_h = layout.size.height;
let mut content_h: f32 = 0.0;
for child in &node.children {
if let Some(child_id) = child.node {
let cl = taffy.layout(child_id).expect("child layout");
let bottom = cl.location.y + cl.size.height;
content_h = content_h.max(bottom);
}
}
let max_scroll = (content_h - container_h).max(0.0);
node.scroll_y = (node.scroll_y - delta_y).clamp(0.0, max_scroll);
}
pub fn scroll_root(node: &mut WidgetNode, delta_y: f32, viewport_h: f32, taffy: &TaffyTree) {
let _ = viewport_h;
scroll_node(node, delta_y, taffy);
}
pub fn update_widget_measures(node: &mut WidgetNode, measures: &[Vec<f32>]) {
node.widget.update_measures(measures);
for child in &mut node.children {
update_widget_measures(child, measures);
}
}
pub fn handle_scrollbar_event(
node: &mut WidgetNode,
taffy: &TaffyTree,
event: &WindowEvent,
) -> bool {
handle_scrollbar_event_offset(node, taffy, event, 0.0, 0.0)
}
fn handle_scrollbar_event_offset(
node: &mut WidgetNode,
taffy: &TaffyTree,
event: &WindowEvent,
parent_x: f32,
parent_y: f32,
) -> bool {
let Some(node_id) = node.node else { return false; };
let layout = taffy.layout(node_id).expect("layout");
let abs_x = parent_x + layout.location.x;
let abs_y = parent_y + layout.location.y;
let child_y = abs_y - node.scroll_y;
for child in &mut node.children {
if handle_scrollbar_event_offset(child, taffy, event, abs_x, child_y) {
return true;
}
}
if !node.widget.is_scrollable() {
return false;
}
let container_h = layout.size.height;
let content_h = content_height(node, taffy);
if content_h <= container_h {
return false;
}
let _track_y = abs_y + SCROLLBAR_MARGIN;
let track_h = container_h - SCROLLBAR_MARGIN * 2.0;
let max_scroll = (content_h - container_h).max(0.0);
let ratio = container_h / content_h;
let thumb_h = (ratio * track_h).max(SCROLLBAR_MIN_THUMB);
match event {
WindowEvent::CursorMoved { position, .. } => {
let _cx = position.x as f32;
let cy = position.y as f32;
if node.scrollbar_dragging {
let delta_y = cy - node.scrollbar_drag_start_y;
let scroll_per_pixel = max_scroll / (track_h - thumb_h);
node.scroll_y = (node.scrollbar_drag_start_scroll + delta_y * scroll_per_pixel)
.clamp(0.0, max_scroll);
return true;
}
false
}
_ => false,
}
}
pub fn try_start_scrollbar_drag(
node: &mut WidgetNode,
taffy: &TaffyTree,
cx: f32,
cy: f32,
) -> bool {
try_start_scrollbar_drag_offset(node, taffy, cx, cy, 0.0, 0.0)
}
fn try_start_scrollbar_drag_offset(
node: &mut WidgetNode,
taffy: &TaffyTree,
cx: f32,
cy: f32,
parent_x: f32,
parent_y: f32,
) -> bool {
let Some(node_id) = node.node else { return false; };
let layout = taffy.layout(node_id).expect("layout");
let abs_x = parent_x + layout.location.x;
let abs_y = parent_y + layout.location.y;
let child_y = abs_y - node.scroll_y;
for child in &mut node.children {
if try_start_scrollbar_drag_offset(child, taffy, cx, cy, abs_x, child_y) {
return true;
}
}
if !node.widget.is_scrollable() {
return false;
}
let container_h = layout.size.height;
let content_h = content_height(node, taffy);
if content_h <= container_h {
return false;
}
let track_x = abs_x + layout.size.width - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN;
let track_y = abs_y + SCROLLBAR_MARGIN;
let track_h = container_h - SCROLLBAR_MARGIN * 2.0;
let in_scrollbar = cx >= track_x
&& cx <= track_x + SCROLLBAR_WIDTH + SCROLLBAR_MARGIN
&& cy >= abs_y
&& cy <= abs_y + container_h;
if !in_scrollbar {
return false;
}
let max_scroll = (content_h - container_h).max(0.0);
let ratio = container_h / content_h;
let thumb_h = (ratio * track_h).max(SCROLLBAR_MIN_THUMB);
let scroll_ratio = if max_scroll > 0.0 { node.scroll_y / max_scroll } else { 0.0 };
let thumb_y = track_y + scroll_ratio * (track_h - thumb_h);
if cy >= thumb_y && cy <= thumb_y + thumb_h {
node.scrollbar_dragging = true;
node.scrollbar_drag_start_y = cy;
node.scrollbar_drag_start_scroll = node.scroll_y;
} else {
let click_ratio = (cy - track_y) / track_h;
node.scroll_y = (click_ratio * max_scroll).clamp(0.0, max_scroll);
}
true
}
pub fn release_scrollbar_drag(node: &mut WidgetNode) {
node.scrollbar_dragging = false;
for child in &mut node.children {
release_scrollbar_drag(child);
}
}
pub fn clear_active_widgets(node: &mut WidgetNode) {
node.widget.clear_active();
for child in &mut node.children {
clear_active_widgets(child);
}
}