1use wgpu::util::DeviceExt;
2use wgpu::{
3 BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor,
4 BindGroupLayoutEntry, BindingType, Buffer, BufferBindingType, BufferUsages, ColorTargetState,
5 ColorWrites, Device, FragmentState, MultisampleState, PipelineLayoutDescriptor, PrimitiveState,
6 PrimitiveTopology, Queue, RenderPass, RenderPipeline, RenderPipelineDescriptor,
7 ShaderModuleDescriptor, ShaderSource, ShaderStages, TextureFormat, VertexState,
8};
9
10pub struct Scrollbar {
12 pipeline: RenderPipeline,
13 uniform_buffer: Buffer,
14 bind_group: BindGroup,
15 track_bind_group: BindGroup,
16 track_uniform_buffer: Buffer,
17 width: f32,
18 visible: bool,
19 position_right: bool, thumb_color: [f32; 4],
21 track_color: [f32; 4],
22
23 scrollbar_x: f32, scrollbar_y: f32, scrollbar_height: f32, window_width: u32,
28 window_height: u32,
29
30 scroll_offset: usize,
32 visible_lines: usize,
33 total_lines: usize,
34}
35
36#[repr(C)]
37#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
38struct ScrollbarUniforms {
39 position: [f32; 2], size: [f32; 2], color: [f32; 4],
44}
45
46impl Scrollbar {
47 pub fn new(
57 device: &Device,
58 format: TextureFormat,
59 width: f32,
60 position: &str,
61 thumb_color: [f32; 4],
62 track_color: [f32; 4],
63 ) -> Self {
64 let shader = device.create_shader_module(ShaderModuleDescriptor {
66 label: Some("Scrollbar Shader"),
67 source: ShaderSource::Wgsl(include_str!("shaders/scrollbar.wgsl").into()),
68 });
69
70 let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
72 label: Some("Scrollbar Bind Group Layout"),
73 entries: &[BindGroupLayoutEntry {
74 binding: 0,
75 visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
76 ty: BindingType::Buffer {
77 ty: BufferBindingType::Uniform,
78 has_dynamic_offset: false,
79 min_binding_size: None,
80 },
81 count: None,
82 }],
83 });
84
85 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
87 label: Some("Scrollbar Pipeline Layout"),
88 bind_group_layouts: &[&bind_group_layout],
89 push_constant_ranges: &[],
90 });
91
92 let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
94 label: Some("Scrollbar Pipeline"),
95 layout: Some(&pipeline_layout),
96 vertex: VertexState {
97 module: &shader,
98 entry_point: Some("vs_main"),
99 buffers: &[],
100 compilation_options: Default::default(),
101 },
102 fragment: Some(FragmentState {
103 module: &shader,
104 entry_point: Some("fs_main"),
105 targets: &[Some(ColorTargetState {
106 format,
107 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
108 write_mask: ColorWrites::ALL,
109 })],
110 compilation_options: Default::default(),
111 }),
112 primitive: PrimitiveState {
113 topology: PrimitiveTopology::TriangleStrip,
114 ..Default::default()
115 },
116 depth_stencil: None,
117 multisample: MultisampleState::default(),
118 multiview: None,
119 cache: None,
120 });
121
122 let thumb_uniforms = ScrollbarUniforms {
128 position: [0.0, 0.0],
129 size: [1.0, 1.0],
130 color: thumb_color,
131 };
132
133 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
134 label: Some("Scrollbar Thumb Uniform Buffer"),
135 contents: bytemuck::cast_slice(&[thumb_uniforms]),
136 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
137 });
138
139 let track_uniforms = ScrollbarUniforms {
141 position: [0.0, 0.0],
142 size: [1.0, 1.0],
143 color: track_color,
144 };
145
146 let track_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
147 label: Some("Scrollbar Track Uniform Buffer"),
148 contents: bytemuck::cast_slice(&[track_uniforms]),
149 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
150 });
151
152 let bind_group = device.create_bind_group(&BindGroupDescriptor {
154 label: Some("Scrollbar Thumb Bind Group"),
155 layout: &bind_group_layout,
156 entries: &[BindGroupEntry {
157 binding: 0,
158 resource: uniform_buffer.as_entire_binding(),
159 }],
160 });
161
162 let track_bind_group = device.create_bind_group(&BindGroupDescriptor {
163 label: Some("Scrollbar Track Bind Group"),
164 layout: &bind_group_layout,
165 entries: &[BindGroupEntry {
166 binding: 0,
167 resource: track_uniform_buffer.as_entire_binding(),
168 }],
169 });
170
171 let position_right = position.eq_ignore_ascii_case("right");
172
173 Self {
174 pipeline,
175 uniform_buffer,
176 bind_group,
177 track_bind_group,
178 track_uniform_buffer,
179 width,
180 visible: false,
181 position_right,
182 thumb_color,
183 track_color,
184 scrollbar_x: 0.0,
185 scrollbar_y: 0.0,
186 scrollbar_height: 0.0,
187 window_width: 0,
188 window_height: 0,
189 scroll_offset: 0,
190 visible_lines: 0,
191 total_lines: 0,
192 }
193 }
194
195 pub fn update(
204 &mut self,
205 queue: &Queue,
206 scroll_offset: usize,
207 visible_lines: usize,
208 total_lines: usize,
209 window_width: u32,
210 window_height: u32,
211 ) {
212 self.scroll_offset = scroll_offset;
214 self.visible_lines = visible_lines;
215 self.total_lines = total_lines;
216 self.window_width = window_width;
217 self.window_height = window_height;
218
219 self.visible = total_lines > visible_lines;
221
222 if !self.visible {
223 return;
224 }
225
226 let viewport_ratio = visible_lines as f32 / total_lines as f32;
228 let scrollbar_height = (viewport_ratio * window_height as f32).max(20.0);
229
230 let max_scroll = total_lines.saturating_sub(visible_lines);
234
235 let clamped_offset = scroll_offset.min(max_scroll);
237
238 let scroll_ratio = if max_scroll > 0 {
239 (clamped_offset as f32 / max_scroll as f32).clamp(0.0, 1.0)
240 } else {
241 0.0
242 };
243
244 let scrollbar_y = ((1.0 - scroll_ratio) * (window_height as f32 - scrollbar_height))
246 .clamp(0.0, window_height as f32 - scrollbar_height);
247
248 self.scrollbar_x = if self.position_right {
251 window_width as f32 - self.width
252 } else {
253 0.0
254 };
255 self.scrollbar_y = scrollbar_y;
256 self.scrollbar_height = scrollbar_height;
257
258 let ndc_width = 2.0 * self.width / window_width as f32;
260 let ndc_x = if self.position_right {
261 1.0 - ndc_width } else {
263 -1.0 };
265
266 let track_ndc_y = -1.0; let track_ndc_height = 2.0; let track_uniforms = ScrollbarUniforms {
270 position: [ndc_x, track_ndc_y],
271 size: [ndc_width, track_ndc_height],
272 color: self.track_color,
273 };
274 queue.write_buffer(
275 &self.track_uniform_buffer,
276 0,
277 bytemuck::cast_slice(&[track_uniforms]),
278 );
279
280 let thumb_bottom = window_height as f32 - (scrollbar_y + scrollbar_height);
282 let thumb_ndc_y = -1.0 + (2.0 * thumb_bottom / window_height as f32);
283 let thumb_ndc_height = 2.0 * scrollbar_height / window_height as f32;
284 let thumb_uniforms = ScrollbarUniforms {
285 position: [ndc_x, thumb_ndc_y],
286 size: [ndc_width, thumb_ndc_height],
287 color: self.thumb_color,
288 };
289 queue.write_buffer(
290 &self.uniform_buffer,
291 0,
292 bytemuck::cast_slice(&[thumb_uniforms]),
293 );
294 }
295
296 pub fn render<'a>(&'a self, render_pass: &mut RenderPass<'a>) {
298 if !self.visible {
299 return;
300 }
301
302 render_pass.set_pipeline(&self.pipeline);
303
304 render_pass.set_bind_group(0, &self.track_bind_group, &[]);
306 render_pass.draw(0..4, 0..1);
307
308 render_pass.set_bind_group(0, &self.bind_group, &[]);
310 render_pass.draw(0..4, 0..1);
311 }
312
313 pub fn update_appearance(&mut self, width: f32, thumb_color: [f32; 4], track_color: [f32; 4]) {
315 self.width = width;
316 self.thumb_color = thumb_color;
317 self.track_color = track_color;
318 }
320
321 #[allow(dead_code)]
323 pub fn update_position(&mut self, position: &str) {
324 self.position_right = !position.eq_ignore_ascii_case("left");
325 }
326
327 #[allow(dead_code)]
328 pub fn width(&self) -> f32 {
329 self.width
330 }
331
332 #[allow(dead_code)]
333 pub fn position_right(&self) -> bool {
334 self.position_right
335 }
336
337 pub fn contains_point(&self, x: f32, y: f32) -> bool {
343 if !self.visible {
344 return false;
345 }
346
347 x >= self.scrollbar_x
348 && x <= self.scrollbar_x + self.width
349 && y >= self.scrollbar_y
350 && y <= self.scrollbar_y + self.scrollbar_height
351 }
352
353 pub fn track_contains_x(&self, x: f32) -> bool {
355 if !self.visible {
356 return false;
357 }
358
359 x >= self.scrollbar_x && x <= self.scrollbar_x + self.width
360 }
361
362 pub fn thumb_bounds(&self) -> Option<(f32, f32)> {
364 if !self.visible {
365 return None;
366 }
367
368 Some((self.scrollbar_y, self.scrollbar_height))
369 }
370
371 pub fn mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
379 if !self.visible {
380 return None;
381 }
382
383 let max_scroll = self.total_lines.saturating_sub(self.visible_lines);
384 if max_scroll == 0 {
385 return Some(0);
386 }
387
388 let track_height = (self.window_height as f32 - self.scrollbar_height).max(1.0);
390
391 let clamped_y = mouse_y.clamp(0.0, track_height);
393
394 let scroll_ratio = 1.0 - (clamped_y / track_height);
396
397 let scroll_offset = (scroll_ratio * max_scroll as f32).round() as usize;
399
400 Some(scroll_offset.min(max_scroll))
401 }
402
403 #[allow(dead_code)]
405 pub fn is_visible(&self) -> bool {
406 self.visible
407 }
408}