rosace_widgets/tree/
list_view.rs1use std::sync::Arc;
2
3use rosace_core::types::{Point, Rect, Size};
4use rosace_layout::Constraints;
5use rosace_render::{Color, DrawCommand};
6use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget, avail_w, avail_h, intersect_rect, ScrollAxes};
7
8pub struct ListView {
34 count: usize,
35 item_extent: f32,
36 builder: Arc<dyn Fn(usize) -> BoxedWidget + Send + Sync>,
37 pub show_scrollbar: bool,
38 pub scrollbar_color: Color,
39}
40
41impl ListView {
42 pub fn builder(
45 count: usize,
46 item_extent: f32,
47 builder: impl Fn(usize) -> BoxedWidget + Send + Sync + 'static,
48 ) -> Self {
49 Self {
50 count,
51 item_extent: item_extent.max(1.0),
52 builder: Arc::new(builder),
53 show_scrollbar: true,
54 scrollbar_color: Color::rgb(50, 55, 85),
55 }
56 }
57
58 pub fn no_scrollbar(mut self) -> Self { self.show_scrollbar = false; self }
59}
60
61impl Widget for ListView {
62 fn layout(&self, ctx: &LayoutCtx) -> Size {
63 let c = ctx.constraints;
66 let h = avail_h(c);
67 Size {
68 width: avail_w(c),
69 height: if h.is_finite() { h } else { self.count as f32 * self.item_extent },
70 }
71 }
72
73 fn paint(&self, ctx: &mut PaintCtx) {
74 let vp = ctx.rect;
75 let ctrl = ctx.scroll_controller();
76 let content_h = self.count as f32 * self.item_extent;
77 let max_scroll = (content_h - vp.size.height).max(0.0);
78 let scroll = ctrl.offset.get()[1].clamp(0.0, max_scroll);
79 let vp_s = [vp.size.width, vp.size.height];
81 if ctrl.viewport_size.get() != vp_s { ctrl.viewport_size.set(vp_s); }
82 let cs = [vp.size.width, content_h];
83 if ctrl.content_size.get() != cs { ctrl.content_size.set(cs); }
84
85 ctx.record(DrawCommand::PushClip { rect: vp });
86 let effective_clip = ctx.clip_rect
87 .and_then(|parent| intersect_rect(parent, vp))
88 .unwrap_or(vp);
89
90 let first = (scroll / self.item_extent).floor().max(0.0) as usize;
92 let last = (((scroll + vp.size.height) / self.item_extent).ceil() as usize)
93 .min(self.count);
94
95 for i in first..last {
96 let row = (self.builder)(i);
97 let row_rect = Rect {
98 origin: Point {
99 x: vp.origin.x,
100 y: vp.origin.y + i as f32 * self.item_extent - scroll,
101 },
102 size: Size { width: vp.size.width, height: self.item_extent },
103 };
104 let lctx = ctx.layout_ctx(Constraints::tight(vp.size.width, self.item_extent));
105 let _ = row.layout(&lctx);
106 let mut row_ctx = ctx.child(row_rect);
107 row_ctx.clip_rect = Some(effective_clip);
108 row.paint(&mut row_ctx);
109 }
110
111 ctx.record(DrawCommand::PopClip);
112
113 let wheel = ctrl.clone();
115 ctx.register_scroll_target(vp, ScrollAxes::Y, Arc::new(move |_dx, dy| {
116 wheel.scroll_by(0.0, -dy);
117 }));
118
119 if self.show_scrollbar && content_h > vp.size.height {
120 let ratio = vp.size.height / content_h;
121 let bar_h = (vp.size.height * ratio).max(16.0);
122 let bar_y = vp.origin.y + (scroll / content_h) * vp.size.height;
123 ctx.fill_rect(Rect {
124 origin: Point { x: vp.origin.x + vp.size.width - 4.0, y: bar_y },
125 size: Size { width: 3.0, height: bar_h },
126 }, self.scrollbar_color);
127 }
128 }
129}