1#![cfg_attr(target_family = "wasm", no_main)]
2
3use gpui::{
4 App, Bounds, Context, ListAlignment, ListState, Render, Window, WindowBounds, WindowOptions,
5 div, list, prelude::*, px, rgb, size,
6};
7use gpui_platform::application;
8
9const ITEM_COUNT: usize = 40;
10const SCROLLBAR_WIDTH: f32 = 12.;
11
12struct BottomListDemo {
13 list_state: ListState,
14}
15
16impl BottomListDemo {
17 fn new() -> Self {
18 Self {
19 list_state: ListState::new(ITEM_COUNT, ListAlignment::Bottom, px(500.)).measure_all(),
20 }
21 }
22}
23
24impl Render for BottomListDemo {
25 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
26 let max_offset = self.list_state.max_offset_for_scrollbar().y;
27 let current_offset = -self.list_state.scroll_px_offset_for_scrollbar().y;
28
29 let viewport_height = self.list_state.viewport_bounds().size.height;
30
31 let raw_fraction = if max_offset > px(0.) {
32 current_offset / max_offset
33 } else {
34 0.
35 };
36
37 let total_height = viewport_height + max_offset;
38 let thumb_height = if total_height > px(0.) {
39 px(viewport_height.as_f32() * viewport_height.as_f32() / total_height.as_f32())
40 .max(px(30.))
41 } else {
42 px(30.)
43 };
44
45 let track_space = viewport_height - thumb_height;
46 let thumb_top = track_space * raw_fraction;
47
48 let bug_detected = raw_fraction > 1.0;
49
50 div()
51 .size_full()
52 .bg(rgb(0xFFFFFF))
53 .flex()
54 .flex_col()
55 .p_4()
56 .gap_2()
57 .child(
58 div()
59 .text_sm()
60 .flex()
61 .flex_col()
62 .gap_1()
63 .child(format!(
64 "offset: {:.0} / max: {:.0} | fraction: {:.3}",
65 current_offset.as_f32(),
66 max_offset.as_f32(),
67 raw_fraction,
68 ))
69 .child(
70 div()
71 .text_color(if bug_detected {
72 rgb(0xCC0000)
73 } else {
74 rgb(0x008800)
75 })
76 .child(if bug_detected {
77 format!(
78 "BUG: fraction is {:.3} (> 1.0) — thumb is off-track!",
79 raw_fraction
80 )
81 } else {
82 "OK: fraction <= 1.0 — thumb is within track.".to_string()
83 }),
84 ),
85 )
86 .child(
87 div()
88 .flex_1()
89 .flex()
90 .flex_row()
91 .overflow_hidden()
92 .border_1()
93 .border_color(rgb(0xCCCCCC))
94 .rounded_sm()
95 .child(
96 list(self.list_state.clone(), |index, _window, _cx| {
97 let height = px(30. + (index % 5) as f32 * 10.);
98 div()
99 .h(height)
100 .w_full()
101 .flex()
102 .items_center()
103 .px_3()
104 .border_b_1()
105 .border_color(rgb(0xEEEEEE))
106 .bg(if index % 2 == 0 {
107 rgb(0xFAFAFA)
108 } else {
109 rgb(0xFFFFFF)
110 })
111 .text_sm()
112 .child(format!("Item {index}"))
113 .into_any()
114 })
115 .flex_1(),
116 )
117 // Scrollbar track
118 .child(
119 div()
120 .w(px(SCROLLBAR_WIDTH))
121 .h_full()
122 .flex_shrink_0()
123 .bg(rgb(0xE0E0E0))
124 .relative()
125 .child(
126 // Thumb — position is unclamped to expose the bug
127 div()
128 .absolute()
129 .top(thumb_top)
130 .w_full()
131 .h(thumb_height)
132 .bg(if bug_detected {
133 rgb(0xCC0000)
134 } else {
135 rgb(0x888888)
136 })
137 .rounded_sm(),
138 ),
139 ),
140 )
141 }
142}
143
144fn run_example() {
145 application().run(|cx: &mut App| {
146 let bounds = Bounds::centered(None, size(px(400.), px(500.)), cx);
147 cx.open_window(
148 WindowOptions {
149 focus: true,
150 window_bounds: Some(WindowBounds::Windowed(bounds)),
151 ..Default::default()
152 },
153 |_, cx| cx.new(|_| BottomListDemo::new()),
154 )
155 .unwrap();
156 cx.activate(true);
157 });
158}
159
160#[cfg(not(target_family = "wasm"))]
161fn main() {
162 run_example();
163}
164
165#[cfg(target_family = "wasm")]
166#[wasm_bindgen::prelude::wasm_bindgen(start)]
167pub fn start() {
168 gpui_platform::web_init();
169 run_example();
170}