#import "modules/std.sx"; #import "modules/math"; #import "ui/types.sx"; #import "ui/render.sx"; #import "ui/events.sx"; #import "ui/view.sx"; ScrollAxes :: enum { vertical; horizontal; both; } ScrollView :: struct { child: ViewChild; offset: Point; content_size: Size; viewport_size: Size; axes: ScrollAxes; dragging: bool; drag_pending: bool; drag_start: Point; drag_offset: Point; SCROLL_SPEED :f32: 20.0; DRAG_THRESHOLD :f32: 4.0; clamp_offset :: (self: *ScrollView) { max_x := max(0.0, self.content_size.width - self.viewport_size.width); max_y := max(0.0, self.content_size.height - self.viewport_size.height); if self.axes == .vertical or self.axes == .both { self.offset.y = clamp(self.offset.y, 0.0, max_y); } else { self.offset.y = 0.0; } if self.axes == .horizontal or self.axes == .both { self.offset.x = clamp(self.offset.x, 0.0, max_x); } else { self.offset.x = 0.0; } } } impl View for ScrollView { size_that_fits :: (self: *ScrollView, proposal: ProposedSize) -> Size { // ScrollView takes all proposed space Size.{ width = proposal.width ?? 0.0, height = proposal.height ?? 0.0 }; } layout :: (self: *ScrollView, bounds: Frame) { self.viewport_size = bounds.size; // Measure child with infinite space on scroll axes child_proposal := ProposedSize.{ width = if self.axes == .horizontal or self.axes == .both then null else bounds.size.width, height = if self.axes == .vertical or self.axes == .both then null else bounds.size.height }; self.content_size = self.child.view.size_that_fits(child_proposal); self.clamp_offset(); // Layout child offset by scroll position self.child.computed_frame = Frame.make( bounds.origin.x - self.offset.x, bounds.origin.y - self.offset.y, self.content_size.width, self.content_size.height ); self.child.view.layout(self.child.computed_frame); } render :: (self: *ScrollView, ctx: *RenderContext, frame: Frame) { ctx.push_clip(frame); self.child.view.render(ctx, self.child.computed_frame); ctx.pop_clip(); } handle_event :: (self: *ScrollView, event: *Event, frame: Frame) -> bool { if pos := event_position(event) { print(" ScrollView.handle_event: pos=({},{}) frame=({},{},{},{})\n", pos.x, pos.y, frame.origin.x, frame.origin.y, frame.size.width, frame.size.height); if !frame.contains(pos) { print(" -> outside frame\n"); return false; } } if event.* == { case .mouse_wheel: (d) { if self.axes == .vertical or self.axes == .both { self.offset.y -= d.delta.y * ScrollView.SCROLL_SPEED; } if self.axes == .horizontal or self.axes == .both { self.offset.x -= d.delta.x * ScrollView.SCROLL_SPEED; } self.clamp_offset(); return true; } case .mouse_down: (d) { // Always record drag start (like Zig preHandleEvent) self.drag_pending = true; self.drag_start = d.position; self.drag_offset = self.offset; // Forward to children — let buttons/tappables handle too self.child.view.handle_event(event, self.child.computed_frame); return true; } case .mouse_moved: (d) { // Activate drag once movement exceeds threshold if self.drag_pending and !self.dragging { dx := d.position.x - self.drag_start.x; dy := d.position.y - self.drag_start.y; dist := sqrt(dx * dx + dy * dy); if dist >= ScrollView.DRAG_THRESHOLD { self.dragging = true; self.drag_pending = false; // Cancel child press state — position far outside so on_tap won't fire cancel :Event = .mouse_up(MouseButtonData.{ position = Point.{ x = 0.0 - 10000.0, y = 0.0 - 10000.0 }, button = .none }); self.child.view.handle_event(@cancel, self.child.computed_frame); } } if self.dragging { if self.axes == .vertical or self.axes == .both { self.offset.y = self.drag_offset.y - (d.position.y - self.drag_start.y); } if self.axes == .horizontal or self.axes == .both { self.offset.x = self.drag_offset.x - (d.position.x - self.drag_start.x); } self.clamp_offset(); return true; } // Forward mouse_moved to children (for hover effects) return self.child.view.handle_event(event, self.child.computed_frame); } case .mouse_up: { was_dragging := self.dragging; self.dragging = false; self.drag_pending = false; if was_dragging { return true; } // Forward to children (for tap completion) return self.child.view.handle_event(event, self.child.computed_frame); } } // Forward other events to child self.child.view.handle_event(event, self.child.computed_frame); } }