1use crate::input::Input;
2use crate::textures::{TextureId, TextureRegistry, UvRect};
3use glyphon::{
4 Attrs, Buffer, Cache, Color as GlyphColor, Family, FontSystem, Metrics, Shaping, SwashCache,
5 TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
6};
7use std::collections::HashMap;
8use std::sync::Arc;
9use winit::{
10 application::ApplicationHandler,
11 event::WindowEvent,
12 event_loop::{ActiveEventLoop, EventLoop},
13 window::{Window, WindowId},
14};
15
16#[must_use]
23pub fn to_ndc(gx: f32, gy: f32, pw: f32, ph: f32) -> [f32; 2] {
24 let gw = grid_width(pw, ph);
25 let unit = ph / 1080.0;
26 let ox = gw.mul_add(-unit, pw) * 0.5;
27 let px = gw.mul_add(0.5, gx).mul_add(unit, ox);
28 let py = (gy + 540.0) * unit;
29 [(px / pw).mul_add(2.0, -1.0), (py / ph).mul_add(-2.0, 1.0)]
30}
31
32#[must_use]
36pub fn grid_width(pw: f32, ph: f32) -> f32 {
37 (pw / ph) * 1080.0
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, serde::Deserialize)]
44pub struct Color {
45 pub r: f32,
46 pub g: f32,
47 pub b: f32,
48 pub a: f32,
49}
50
51impl Color {
52 #[must_use]
54 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
55 Self { r, g, b, a }
56 }
57 pub const WHITE: Self = Self::rgba(1.0, 1.0, 1.0, 1.0);
58
59 fn to_glyph(self) -> GlyphColor {
60 #[expect(
61 clippy::cast_possible_truncation,
62 clippy::cast_sign_loss,
63 reason = "value is clamped to 0.0..=255.0 and rounded before cast"
64 )]
65 fn channel(v: f32) -> u8 {
66 (v * 255.0).clamp(0.0, 255.0).round() as u8
67 }
68 GlyphColor::rgba(
69 channel(self.r),
70 channel(self.g),
71 channel(self.b),
72 channel(self.a),
73 )
74 }
75}
76
77#[derive(Debug, Clone, Copy)]
81pub struct Rect {
82 pub x: f32,
83 pub y: f32,
84 pub w: f32,
85 pub h: f32,
86}
87
88impl Rect {
89 #[must_use]
91 pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
92 Self { x, y, w, h }
93 }
94
95 #[must_use]
97 pub fn contains(&self, px: f32, py: f32) -> bool {
98 px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub struct ShaderId(usize);
105
106impl ShaderId {
107 pub(crate) const fn new(index: usize) -> Self {
109 Self(index)
110 }
111 pub(crate) const fn index(self) -> usize {
113 self.0
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq)]
123pub struct ClipRect {
124 pub x: f32,
125 pub y: f32,
126 pub w: f32,
127 pub h: f32,
128}
129
130fn clip_to_pixels(c: ClipRect, pw: f32, ph: f32) -> (f32, f32, f32, f32) {
135 let unit = ph / 1080.0;
136 let gw = grid_width(pw, ph);
137 let ox = gw.mul_add(-unit, pw) * 0.5;
138 let left = gw.mul_add(0.5, c.x).mul_add(unit, ox);
139 let top = (c.y + 540.0) * unit;
140 let right = gw.mul_add(0.5, c.x + c.w).mul_add(unit, ox);
141 let bottom = (c.y + c.h + 540.0) * unit;
142 (left, top, right, bottom)
143}
144
145struct Batch {
148 shader: ShaderId,
149 verts: Vec<[f32; 2]>,
150 color: Color,
151 z: f32,
152 clip: Option<ClipRect>,
153 texture: Option<TextureId>,
154 uv_rect: Option<UvRect>,
155 corner_radius: f32,
156 border_width: f32,
157 state: [f32; 4],
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, Default)]
164pub enum TextAlign {
165 Left,
166 #[default]
167 Center,
168 Right,
169}
170
171type BatchRange = (
173 ShaderId,
174 std::ops::Range<u32>,
175 Option<ClipRect>,
176 Option<TextureId>,
177 f32,
178);
179
180pub(crate) struct TextDraw<'a> {
182 pub text: &'a str,
183 pub x: f32,
184 pub y: f32,
185 pub w: f32,
186 pub size: f32,
187 pub color: Color,
188 pub align: TextAlign,
189 pub font: Option<&'a str>,
190 pub bold: bool,
191 pub italic: bool,
192 pub clip: Option<ClipRect>,
193 pub z: f32,
194}
195
196pub(crate) struct GpuFrame<'a> {
198 pub encoder: &'a mut wgpu::CommandEncoder,
199 pub view: &'a wgpu::TextureView,
200 pub device: &'a wgpu::Device,
201 pub queue: &'a wgpu::Queue,
202}
203
204struct TextEntry {
207 text: String,
208 x: f32,
209 y: f32,
210 w: f32,
211 size: f32,
212 color: Color,
213 align: TextAlign,
214 font: Option<String>,
215 bold: bool,
216 italic: bool,
217 clip: Option<ClipRect>,
218 z: f32,
219}
220
221pub struct Scene {
227 batches: Vec<Batch>,
228 texts: Vec<TextEntry>,
229}
230
231impl Scene {
232 #[must_use]
234 pub const fn new() -> Self {
235 Self {
236 batches: Vec::new(),
237 texts: Vec::new(),
238 }
239 }
240
241 pub fn push_full(
245 &mut self,
246 verts: Vec<[f32; 2]>,
247 shader: ShaderId,
248 color: Color,
249 z: f32,
250 clip: Option<ClipRect>,
251 corner_radius: f32,
252 ) {
253 self.push_widget(verts, shader, color, z, clip, corner_radius, 0.0, [0.0; 4]);
254 }
255
256 pub fn push_widget(
261 &mut self,
262 verts: Vec<[f32; 2]>,
263 shader: ShaderId,
264 color: Color,
265 z: f32,
266 clip: Option<ClipRect>,
267 corner_radius: f32,
268 border_width: f32,
269 state: [f32; 4],
270 ) {
271 self.batches.push(Batch {
272 shader,
273 verts,
274 color,
275 z,
276 clip,
277 texture: None,
278 uv_rect: None,
279 corner_radius,
280 border_width,
281 state,
282 });
283 }
284
285 pub fn push_image(
287 &mut self,
288 rect: Rect,
289 shader: ShaderId,
290 texture: TextureId,
291 z: f32,
292 clip: Option<ClipRect>,
293 ) {
294 self.push_image_uv(rect, shader, texture, z, clip, None);
295 }
296
297 pub fn push_image_uv(
299 &mut self,
300 rect: Rect,
301 shader: ShaderId,
302 texture: TextureId,
303 depth: f32,
304 clip: Option<ClipRect>,
305 uv_rect: Option<UvRect>,
306 ) {
307 let (rx, ry, rw, rh) = (rect.x, rect.y, rect.w, rect.h);
308 let verts = vec![
309 [rx, ry],
310 [rx + rw, ry],
311 [rx, ry + rh],
312 [rx + rw, ry],
313 [rx + rw, ry + rh],
314 [rx, ry + rh],
315 ];
316 self.batches.push(Batch {
317 shader,
318 verts,
319 color: Color::WHITE,
320 z: depth,
321 clip,
322 texture: Some(texture),
323 uv_rect,
324 corner_radius: 0.0,
325 border_width: 0.0,
326 state: [0.0; 4],
327 });
328 }
329
330 pub fn text(&mut self, text: &str, x: f32, y: f32, w: f32, size: f32, color: Color) {
332 self.push_text(&TextDraw {
333 text,
334 x,
335 y,
336 w,
337 size,
338 color,
339 align: TextAlign::Center,
340 font: None,
341 bold: false,
342 italic: false,
343 clip: None,
344 z: 0.5,
345 });
346 }
347
348 pub fn text_left(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
350 self.push_text(&TextDraw {
351 text,
352 x,
353 y,
354 w: 0.0,
355 size,
356 color,
357 align: TextAlign::Left,
358 font: None,
359 bold: false,
360 italic: false,
361 clip: None,
362 z: 0.5,
363 });
364 }
365
366 pub(crate) fn push_text(&mut self, p: &TextDraw<'_>) {
368 self.texts.push(TextEntry {
369 text: p.text.to_string(),
370 x: p.x,
371 y: p.y,
372 w: p.w,
373 size: p.size,
374 color: p.color,
375 align: p.align,
376 font: p.font.map(std::string::ToString::to_string),
377 bold: p.bold,
378 italic: p.italic,
379 clip: p.clip,
380 z: p.z,
381 });
382 }
383
384 pub fn clear(&mut self) {
386 self.batches.clear();
387 self.texts.clear();
388 }
389 #[must_use]
391 pub const fn is_empty(&self) -> bool {
392 self.batches.is_empty() && self.texts.is_empty()
393 }
394}
395
396impl Default for Scene {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402const VERT_SIZE: u64 = 64;
405const MAX_VERTS: u64 = 2_000_000;
406
407const GLOBALS_SIZE: u64 = (std::mem::size_of::<f32>() * 8) as u64;
409
410struct TextSystem {
413 font_system: FontSystem,
414 swash_cache: SwashCache,
415 _cache: Cache,
416 atlas: TextAtlas,
417 renderer: TextRenderer,
418 viewport: Viewport,
419}
420
421impl TextSystem {
422 fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
423 let font_system = FontSystem::new();
424 let swash_cache = SwashCache::new();
425 let cache = Cache::new(device);
426 let viewport = Viewport::new(device, &cache);
427 let mut atlas = TextAtlas::new(device, queue, &cache, format);
428 let renderer =
429 TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
430 Self {
431 font_system,
432 swash_cache,
433 _cache: cache,
434 atlas,
435 renderer,
436 viewport,
437 }
438 }
439
440 fn build_buffer(&mut self, t: &TextEntry, unit: f32, pw: f32, ph: f32) -> Buffer {
441 let font_size = t.size * unit;
442 let use_left = matches!(t.align, TextAlign::Left) || t.w <= 0.0;
443 let buf_w = if use_left { pw } else { t.w * unit };
444 let family = t.font.as_deref().map_or(Family::SansSerif, Family::Name);
445 let weight = if t.bold {
446 glyphon::Weight::BOLD
447 } else {
448 glyphon::Weight::NORMAL
449 };
450 let style = if t.italic {
451 glyphon::Style::Italic
452 } else {
453 glyphon::Style::Normal
454 };
455 let mut buf = Buffer::new(
456 &mut self.font_system,
457 Metrics::new(font_size, font_size * 1.2),
458 );
459 buf.set_size(&mut self.font_system, Some(buf_w), Some(ph));
460 buf.set_text(
461 &mut self.font_system,
462 &t.text,
463 &Attrs::new().family(family).weight(weight).style(style),
464 Shaping::Advanced,
465 None,
466 );
467 let align = match t.align {
468 TextAlign::Left => glyphon::cosmic_text::Align::Left,
469 TextAlign::Center => glyphon::cosmic_text::Align::Center,
470 TextAlign::Right => glyphon::cosmic_text::Align::Right,
471 };
472 for line in &mut buf.lines {
473 line.set_align(Some(align));
474 }
475 buf.shape_until_scroll(&mut self.font_system, false);
476 buf
477 }
478
479 fn build_text_area<'a>(
480 t: &TextEntry,
481 buf: &'a Buffer,
482 unit: f32,
483 pw: f32,
484 ph: f32,
485 gw: f32,
486 ox: f32,
487 ) -> TextArea<'a> {
488 let bounds = t.clip.map_or_else(
489 || TextBounds {
490 left: 0,
491 top: 0,
492 right: pw.max(0.0).trunc() as i32,
493 bottom: ph.max(0.0).trunc() as i32,
494 },
495 |c| {
496 let (l, t, r, b) = clip_to_pixels(c, pw, ph);
497 TextBounds {
498 left: l.trunc() as i32,
499 top: t.trunc() as i32,
500 right: r.trunc() as i32,
501 bottom: b.trunc() as i32,
502 }
503 },
504 );
505 TextArea {
506 buffer: buf,
507 left: gw.mul_add(0.5, t.x).mul_add(unit, ox),
508 top: (t.y + 540.0) * unit,
509 scale: 1.0,
510 bounds,
511 default_color: t.color.to_glyph(),
512 custom_glyphs: &[],
513 }
514 }
515
516 pub(crate) fn measure_cursor_px(
522 &mut self,
523 text: &str,
524 font_size_px: f32,
525 buf_w_px: f32,
526 _ph: f32,
527 align: TextAlign,
528 cursor_byte: usize,
529 bold: bool,
530 italic: bool,
531 font: Option<&str>,
532 ) -> f32 {
533 let family = font.map_or(Family::SansSerif, Family::Name);
534 let weight = if bold {
535 glyphon::Weight::BOLD
536 } else {
537 glyphon::Weight::NORMAL
538 };
539 let style = if italic {
540 glyphon::Style::Italic
541 } else {
542 glyphon::Style::Normal
543 };
544 let mut buf = Buffer::new(
545 &mut self.font_system,
546 Metrics::new(font_size_px, font_size_px * 1.2),
547 );
548 buf.set_size(&mut self.font_system, Some(buf_w_px), None);
552 buf.set_text(
553 &mut self.font_system,
554 text,
555 &Attrs::new().family(family).weight(weight).style(style),
556 Shaping::Advanced,
557 None,
558 );
559 let cosmic_align = match align {
560 TextAlign::Left => glyphon::cosmic_text::Align::Left,
561 TextAlign::Center => glyphon::cosmic_text::Align::Center,
562 TextAlign::Right => glyphon::cosmic_text::Align::Right,
563 };
564 for line in &mut buf.lines {
565 line.set_align(Some(cosmic_align));
566 }
567 buf.shape_until_scroll(&mut self.font_system, false);
568
569 for run in buf.layout_runs() {
570 let glyphs = run.glyphs;
571 for (i, glyph) in glyphs.iter().enumerate() {
572 if cursor_byte >= glyph.start && cursor_byte < glyph.end {
573 return glyph.x;
574 }
575 if cursor_byte == glyph.end {
576 return glyphs.get(i + 1).map_or(glyph.x + glyph.w, |next| next.x);
577 }
578 }
579 }
580 buf.layout_runs()
582 .last()
583 .and_then(|r| r.glyphs.last())
584 .map_or_else(
585 || match align {
586 TextAlign::Center => buf_w_px * 0.5,
587 TextAlign::Right => buf_w_px,
588 TextAlign::Left => 0.0,
589 },
590 |g| g.x + g.w,
591 )
592 }
593
594 fn render(
595 &mut self,
596 device: &wgpu::Device,
597 queue: &wgpu::Queue,
598 pass: &mut wgpu::RenderPass,
599 texts: &[TextEntry],
600 pw: f32,
601 ph: f32,
602 ) {
603 if texts.is_empty() {
604 return;
605 }
606 let unit = ph / 1080.0;
607 let ox = grid_width(pw, ph).mul_add(-unit, pw) * 0.5; #[expect(
609 clippy::cast_possible_truncation,
610 clippy::cast_sign_loss,
611 reason = "viewport dimensions are clamped to >=0 before cast"
612 )]
613 self.viewport.update(
614 queue,
615 glyphon::Resolution {
616 width: pw.max(0.0) as u32,
617 height: ph.max(0.0) as u32,
618 },
619 );
620
621 let buffers: Vec<Buffer> = texts
622 .iter()
623 .map(|t| self.build_buffer(t, unit, pw, ph))
624 .collect();
625
626 let gw = grid_width(pw, ph);
627 let areas: Vec<TextArea> = texts
628 .iter()
629 .zip(buffers.iter())
630 .map(|(t, buf)| Self::build_text_area(t, buf, unit, pw, ph, gw, ox))
631 .collect();
632
633 self.atlas.trim();
634 if let Err(e) = self.renderer.prepare(
635 device,
636 queue,
637 &mut self.font_system,
638 &mut self.atlas,
639 &self.viewport,
640 areas,
641 &mut self.swash_cache,
642 ) {
643 eprintln!("[pane] text prepare error: {e}");
644 return;
645 }
646 if let Err(e) = self.renderer.render(&self.atlas, &self.viewport, pass) {
647 eprintln!("[pane] text render error: {e}");
648 }
649 }
650}
651
652struct StandaloneReady {
667 surface: wgpu::Surface<'static>,
668 config: wgpu::SurfaceConfiguration,
669 device: Arc<wgpu::Device>,
670 queue: Arc<wgpu::Queue>,
671}
672
673struct GpuResources {
674 vertex_buf: wgpu::Buffer,
675 text: TextSystem,
676 text_overlay: TextSystem,
677 globals_bgl: wgpu::BindGroupLayout,
678 globals_buf: wgpu::Buffer,
679 globals_bg: wgpu::BindGroup,
680}
681
682pub struct Pane {
690 pipelines: Vec<wgpu::RenderPipeline>,
691 shader_cache: HashMap<String, ShaderId>,
692 gpu: Option<GpuResources>, format: wgpu::TextureFormat,
694 standalone: Option<StandaloneReady>, total_time: f32,
696 frame_count: u32,
697}
698
699impl Pane {
700 const fn gpu(&self) -> &GpuResources {
701 self.gpu.as_ref().unwrap()
702 }
703 const fn gpu_mut(&mut self) -> &mut GpuResources {
704 self.gpu.as_mut().unwrap()
705 }
706
707 fn make_gpu(
708 device: &wgpu::Device,
709 queue: &wgpu::Queue,
710 format: wgpu::TextureFormat,
711 ) -> GpuResources {
712 let vertex_buf = device.create_buffer(&wgpu::BufferDescriptor {
713 label: None,
714 size: MAX_VERTS * VERT_SIZE,
715 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
716 mapped_at_creation: false,
717 });
718 let globals_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
719 label: None,
720 entries: &[wgpu::BindGroupLayoutEntry {
721 binding: 0,
722 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
723 ty: wgpu::BindingType::Buffer {
724 ty: wgpu::BufferBindingType::Uniform,
725 has_dynamic_offset: false,
726 min_binding_size: None,
727 },
728 count: None,
729 }],
730 });
731 let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
732 label: None,
733 size: GLOBALS_SIZE,
734 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
735 mapped_at_creation: false,
736 });
737 let globals_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
738 label: None,
739 layout: &globals_bgl,
740 entries: &[wgpu::BindGroupEntry {
741 binding: 0,
742 resource: globals_buf.as_entire_binding(),
743 }],
744 });
745 GpuResources {
746 vertex_buf,
747 text: TextSystem::new(device, queue, format),
748 text_overlay: TextSystem::new(device, queue, format),
749 globals_bgl,
750 globals_buf,
751 globals_bg,
752 }
753 }
754
755 #[must_use]
757 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
758 Self {
759 pipelines: Vec::new(),
760 shader_cache: HashMap::new(),
761 gpu: Some(Self::make_gpu(device, queue, format)),
762 format,
763 standalone: None,
764 total_time: 0.0,
765 frame_count: 0,
766 }
767 }
768
769 fn new_standalone() -> Self {
771 Self {
772 pipelines: Vec::new(),
773 shader_cache: HashMap::new(),
774 gpu: None,
775 format: wgpu::TextureFormat::Bgra8UnormSrgb,
776 standalone: None,
777 total_time: 0.0,
778 frame_count: 0,
779 }
780 }
781
782 async fn init(&mut self, window: Arc<Window>) -> Result<(), String> {
783 let size = window.inner_size();
784 let instance = wgpu::Instance::default();
785 let surface = instance
786 .create_surface(window)
787 .map_err(|e| format!("[pane] failed to create surface: {e}"))?;
788 let adapter = instance
789 .request_adapter(&wgpu::RequestAdapterOptions {
790 compatible_surface: Some(&surface),
791 ..Default::default()
792 })
793 .await
794 .map_err(|e| format!("[pane] no compatible GPU adapter found: {e}"))?;
795 let (device, queue) = adapter
796 .request_device(&wgpu::DeviceDescriptor::default())
797 .await
798 .map_err(|e| format!("[pane] failed to acquire GPU device: {e}"))?;
799 let cap = surface.get_capabilities(&adapter);
800 let format = cap
803 .formats
804 .iter()
805 .find(|f| f.is_srgb())
806 .copied()
807 .unwrap_or(cap.formats[0]);
808 let config = wgpu::SurfaceConfiguration {
809 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
810 format,
811 width: size.width,
812 height: size.height,
813 present_mode: wgpu::PresentMode::Fifo,
814 alpha_mode: cap.alpha_modes[0],
815 view_formats: vec![],
816 desired_maximum_frame_latency: 2,
817 };
818 surface.configure(&device, &config);
819
820 let device = Arc::new(device);
821 let queue = Arc::new(queue);
822 self.gpu = Some(Self::make_gpu(&device, &queue, format));
823 self.format = format;
824 self.standalone = Some(StandaloneReady {
825 surface,
826 config,
827 device,
828 queue,
829 });
830 Ok(())
831 }
832
833 fn advance_time(
836 &mut self,
837 dt: f32,
838 pw: f32,
839 ph: f32,
840 cursor: [f32; 3],
841 queue: &wgpu::Queue,
842 tex_reg: &mut TextureRegistry,
843 ) {
844 self.total_time += dt;
845 self.frame_count = self.frame_count.wrapping_add(1);
846 let [cx, cy] = to_ndc(cursor[0], cursor[1], pw, ph);
847 let globals_data: [f32; 8] = [
848 self.total_time,
849 dt,
850 self.frame_count as f32,
851 cx,
852 cy,
853 cursor[2],
854 0.0,
855 0.0,
856 ];
857 queue.write_buffer(
858 &self.gpu().globals_buf,
859 0,
860 bytemuck::cast_slice(&globals_data),
861 );
862 tex_reg.update(dt);
863 }
864
865 pub(crate) fn device(&self) -> Option<&Arc<wgpu::Device>> {
867 self.standalone.as_ref().map(|s| &s.device)
868 }
869
870 pub(crate) fn queue(&self) -> Option<&Arc<wgpu::Queue>> {
872 self.standalone.as_ref().map(|s| &s.queue)
873 }
874
875 pub fn register_shader(
881 &mut self,
882 device: &wgpu::Device,
883 wgsl: &str,
884 tex_reg: &TextureRegistry,
885 ) -> ShaderId {
886 if let Some(&id) = self.shader_cache.get(wgsl) {
887 return id;
888 }
889
890 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
891 label: None,
892 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
893 });
894 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
895 label: None,
896 bind_group_layouts: &[
897 Some(&tex_reg.bind_group_layout),
898 Some(&self.gpu().globals_bgl),
899 ],
900 immediate_size: 0,
901 });
902 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
903 label: None,
904 layout: Some(&layout),
905 vertex: wgpu::VertexState {
906 module: &shader,
907 entry_point: Some("vs"),
908 buffers: &[wgpu::VertexBufferLayout {
909 array_stride: VERT_SIZE,
910 step_mode: wgpu::VertexStepMode::Vertex,
911 attributes: &[
912 wgpu::VertexAttribute {
913 offset: 0,
914 shader_location: 0,
915 format: wgpu::VertexFormat::Float32x2,
916 },
917 wgpu::VertexAttribute {
918 offset: 8,
919 shader_location: 1,
920 format: wgpu::VertexFormat::Float32x2,
921 },
922 wgpu::VertexAttribute {
923 offset: 16,
924 shader_location: 2,
925 format: wgpu::VertexFormat::Float32x4,
926 },
927 wgpu::VertexAttribute {
928 offset: 32,
929 shader_location: 3,
930 format: wgpu::VertexFormat::Float32x2,
931 },
932 wgpu::VertexAttribute {
933 offset: 40,
934 shader_location: 4,
935 format: wgpu::VertexFormat::Float32x2,
936 },
937 wgpu::VertexAttribute {
938 offset: 48,
939 shader_location: 5,
940 format: wgpu::VertexFormat::Float32x4,
941 },
942 ],
943 }],
944 compilation_options: wgpu::PipelineCompilationOptions::default(),
945 },
946 fragment: Some(wgpu::FragmentState {
947 module: &shader,
948 entry_point: Some("fs"),
949 targets: &[Some(wgpu::ColorTargetState {
950 format: self.format,
951 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
952 write_mask: wgpu::ColorWrites::ALL,
953 })],
954 compilation_options: wgpu::PipelineCompilationOptions::default(),
955 }),
956 primitive: wgpu::PrimitiveState::default(),
957 depth_stencil: None,
958 multisample: wgpu::MultisampleState::default(),
959 multiview_mask: None,
960 cache: None,
961 });
962
963 let id = ShaderId::new(self.pipelines.len());
964 self.pipelines.push(pipeline);
965 self.shader_cache.insert(wgsl.to_string(), id);
966 id
967 }
968
969 pub(crate) fn measure_cursor(
975 &mut self,
976 text: &str,
977 font_size_grid: f32,
978 rect_w_grid: f32,
979 align: TextAlign,
980 pw: f32,
981 ph: f32,
982 cursor_byte: usize,
983 bold: bool,
984 italic: bool,
985 font: Option<&str>,
986 ) -> f32 {
987 let unit = ph / 1080.0;
988 let font_size_px = font_size_grid * unit;
989 let buf_w_px = match align {
991 TextAlign::Left => pw,
992 _ => (rect_w_grid * unit).max(1.0),
993 };
994 self.gpu_mut().text.measure_cursor_px(
995 text,
996 font_size_px,
997 buf_w_px,
998 ph,
999 align,
1000 cursor_byte,
1001 bold,
1002 italic,
1003 font,
1004 ) / unit
1005 }
1006
1007 pub(crate) fn render(
1009 &mut self,
1010 frame: GpuFrame<'_>,
1011 scene: &mut Scene,
1012 pw: f32,
1013 ph: f32,
1014 dt: f32,
1015 cursor: [f32; 3],
1016 clear_color: Option<crate::draw::Color>,
1017 tex_reg: &mut TextureRegistry,
1018 ) {
1019 let GpuFrame {
1020 encoder,
1021 view,
1022 device,
1023 queue,
1024 } = frame;
1025 self.advance_time(dt, pw, ph, cursor, queue, tex_reg);
1026 if scene.is_empty() {
1027 return;
1028 }
1029 let load_op = clear_color.map_or(wgpu::LoadOp::Load, |c| {
1030 wgpu::LoadOp::Clear(wgpu::Color {
1031 r: f64::from(c.r),
1032 g: f64::from(c.g),
1033 b: f64::from(c.b),
1034 a: f64::from(c.a),
1035 })
1036 });
1037 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1038 label: None,
1039 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1040 view,
1041 resolve_target: None,
1042 depth_slice: None,
1043 ops: wgpu::Operations {
1044 load: load_op,
1045 store: wgpu::StoreOp::Store,
1046 },
1047 })],
1048 depth_stencil_attachment: None,
1049 occlusion_query_set: None,
1050 timestamp_writes: None,
1051 multiview_mask: None,
1052 });
1053 let ranges = self.build_batches(scene, pw, ph, queue);
1056 scene.texts.sort_by(|a, b| a.z.total_cmp(&b.z));
1057 let split = scene.texts.partition_point(|t| t.z < 1.0);
1058
1059 self.issue_draw_calls(&mut pass, pw, ph, &ranges, 0.0, 1.0, tex_reg);
1061 self.gpu_mut()
1062 .text
1063 .render(device, queue, &mut pass, &scene.texts[..split], pw, ph);
1064
1065 self.issue_draw_calls(&mut pass, pw, ph, &ranges, 1.0, f32::MAX, tex_reg);
1069 self.gpu_mut()
1070 .text_overlay
1071 .render(device, queue, &mut pass, &scene.texts[split..], pw, ph);
1072 }
1073
1074 pub(crate) fn present_standalone(
1076 &mut self,
1077 scene: &mut Scene,
1078 pw: f32,
1079 ph: f32,
1080 dt: f32,
1081 cursor: [f32; 3],
1082 clear_color: Option<crate::draw::Color>,
1083 tex_reg: &mut TextureRegistry,
1084 ) {
1085 let Some(sa) = &self.standalone else { return };
1086 let queue = sa.queue.clone();
1088 self.advance_time(dt, pw, ph, cursor, &queue, tex_reg);
1089 if scene.is_empty() {
1090 return;
1091 }
1092
1093 let sa = self.standalone.as_ref().unwrap();
1094 let output = match sa.surface.get_current_texture() {
1095 wgpu::CurrentSurfaceTexture::Success(t)
1096 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
1097 wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
1098 sa.surface.configure(&sa.device, &sa.config);
1099 return;
1100 }
1101 other => {
1102 eprintln!("[pane_ui] Surface error: {other:?}");
1103 return;
1104 }
1105 };
1106
1107 let view = output
1108 .texture
1109 .create_view(&wgpu::TextureViewDescriptor::default());
1110 let device = sa.device.clone();
1111 let queue = sa.queue.clone();
1112 let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1113 self.render(
1114 GpuFrame {
1115 encoder: &mut enc,
1116 view: &view,
1117 device: &device,
1118 queue: &queue,
1119 },
1120 scene,
1121 pw,
1122 ph,
1123 dt,
1124 cursor,
1125 clear_color,
1126 tex_reg,
1127 );
1128 queue.submit(std::iter::once(enc.finish()));
1129 output.present();
1130 }
1131
1132 fn resize(&mut self, w: u32, h: u32) {
1134 if let Some(sa) = &mut self.standalone {
1135 sa.config.width = w;
1136 sa.config.height = h;
1137 sa.surface.configure(&sa.device, &sa.config);
1138 }
1139 }
1140
1141 fn build_batches(
1144 &self,
1145 scene: &mut Scene,
1146 pw: f32,
1147 ph: f32,
1148 queue: &wgpu::Queue,
1149 ) -> Vec<BatchRange> {
1150 if scene.batches.is_empty() {
1151 return Vec::new();
1152 }
1153 scene.batches.sort_by(|a, b| a.z.total_cmp(&b.z));
1154
1155 let mut all_verts: Vec<f32> = Vec::new();
1156 let mut ranges: Vec<BatchRange> = Vec::new();
1157
1158 for batch in &scene.batches {
1159 let start = u32::try_from(all_verts.len() / 16).unwrap_or(0);
1160
1161 let min_x = batch.verts.iter().map(|v| v[0]).fold(f32::MAX, f32::min);
1162 let max_x = batch.verts.iter().map(|v| v[0]).fold(f32::MIN, f32::max);
1163 let min_y = batch.verts.iter().map(|v| v[1]).fold(f32::MAX, f32::min);
1164 let max_y = batch.verts.iter().map(|v| v[1]).fold(f32::MIN, f32::max);
1165 let rw = (max_x - min_x).max(0.0001);
1166 let rh = (max_y - min_y).max(0.0001);
1167 let uv = batch.uv_rect.unwrap_or(UvRect::FULL);
1168 let unit = ph / 1080.0;
1169 let cr_px = (batch.corner_radius * unit).min(rw.min(rh) * unit * 0.5);
1170 let bw_px = batch.border_width * unit;
1171 let size_w = rw * unit;
1172 let size_h = rh * unit;
1173 let s = batch.state;
1174
1175 for v in &batch.verts {
1176 let [x, y] = to_ndc(v[0], v[1], pw, ph);
1177 let t_u = (v[0] - min_x) / rw;
1178 let t_v = (v[1] - min_y) / rh;
1179 let u = uv.u_min + t_u * (uv.u_max - uv.u_min);
1180 let vv = uv.v_min + t_v * (uv.v_max - uv.v_min);
1181 all_verts.extend_from_slice(&[
1182 x,
1183 y,
1184 u,
1185 vv,
1186 batch.color.r,
1187 batch.color.g,
1188 batch.color.b,
1189 batch.color.a,
1190 cr_px,
1191 bw_px,
1192 size_w,
1193 size_h,
1194 s[0],
1195 s[1],
1196 s[2],
1197 s[3],
1198 ]);
1199 }
1200 ranges.push((
1201 batch.shader,
1202 start..u32::try_from(all_verts.len() / 16).unwrap_or(0),
1203 batch.clip,
1204 batch.texture,
1205 batch.z,
1206 ));
1207 }
1208
1209 if all_verts.len() > (MAX_VERTS * 16) as usize {
1211 eprintln!(
1212 "[pane] vertex buffer overflow: {} verts (max {}); frame skipped",
1213 all_verts.len() / 16,
1214 MAX_VERTS
1215 );
1216 return Vec::new();
1217 }
1218 queue.write_buffer(&self.gpu().vertex_buf, 0, bytemuck::cast_slice(&all_verts));
1219 ranges
1220 }
1221
1222 fn issue_draw_calls(
1224 &self,
1225 pass: &mut wgpu::RenderPass,
1226 pw: f32,
1227 ph: f32,
1228 ranges: &[BatchRange],
1229 z_min: f32,
1230 z_max: f32,
1231 tex_reg: &TextureRegistry,
1232 ) {
1233 if ranges
1234 .iter()
1235 .all(|(_, _, _, _, z)| *z < z_min || *z >= z_max)
1236 {
1237 return;
1238 }
1239 pass.set_vertex_buffer(0, self.gpu().vertex_buf.slice(..));
1241 pass.set_bind_group(1, &self.gpu().globals_bg, &[]);
1242 let mut current_clip: Option<ClipRect> = None;
1243 for (shader_id, range, clip, texture, z) in ranges {
1244 if *z < z_min || *z >= z_max {
1245 continue;
1246 }
1247 if *clip != current_clip {
1248 match clip {
1249 Some(c) => {
1250 let (l, t, r, b) = clip_to_pixels(*c, pw, ph);
1251 let max_w = pw.max(0.0) as u32;
1252 let max_h = ph.max(0.0) as u32;
1253 let sx = (l.max(0.0) as u32).min(max_w);
1254 let sy = (t.max(0.0) as u32).min(max_h);
1255 let sw = ((r - l).max(0.0) as u32).min(max_w - sx);
1256 let sh = ((b - t).max(0.0) as u32).min(max_h - sy);
1257 if sw > 0 && sh > 0 {
1258 pass.set_scissor_rect(sx, sy, sw, sh);
1259 }
1260 }
1261 None => pass.set_scissor_rect(0, 0, pw.max(0.0) as u32, ph.max(0.0) as u32),
1262 }
1263 current_clip = *clip;
1264 }
1265 pass.set_pipeline(&self.pipelines[shader_id.index()]);
1266 if let Some(id) = texture
1267 && tex_reg.is_hidden(*id)
1268 {
1269 continue;
1270 }
1271 let bg = texture.map_or_else(|| tex_reg.dummy(), |id| tex_reg.current_bind_group(id));
1272 pass.set_bind_group(0, bg, &[]);
1273 pass.draw(range.clone(), 0..1);
1274 }
1275 pass.set_scissor_rect(0, 0, pw.max(0.0) as u32, ph.max(0.0) as u32);
1276 }
1277}
1278
1279struct App<F: FnMut(&mut Pane, &mut Scene, &mut Input, f32, f32, f32)> {
1282 window: Option<Arc<Window>>,
1283 pane: Pane,
1284 scene: Scene,
1285 input: Input,
1286 draw_fn: F,
1287 last_frame: std::time::Instant,
1288}
1289
1290impl<F: FnMut(&mut Pane, &mut Scene, &mut Input, f32, f32, f32) + 'static> ApplicationHandler
1291 for App<F>
1292{
1293 fn resumed(&mut self, el: &ActiveEventLoop) {
1294 let win = Arc::new(
1295 el.create_window(Window::default_attributes().with_title("Pane"))
1296 .unwrap(),
1297 );
1298 if let Err(e) = pollster::block_on(self.pane.init(win.clone())) {
1299 eprintln!("{e}");
1300 el.exit();
1301 return;
1302 }
1303 self.window = Some(win);
1304 }
1305
1306 fn window_event(&mut self, el: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
1307 if let Some(win) = &self.window {
1308 let s = win.inner_size();
1309 self.input
1310 .handle_event(&event, s.width as f32, s.height as f32);
1311 }
1312 match event {
1313 WindowEvent::CloseRequested => el.exit(),
1314 WindowEvent::Resized(s) => self.pane.resize(s.width, s.height),
1315 WindowEvent::RedrawRequested => {
1316 if let Some(win) = &self.window {
1317 let s = win.inner_size();
1318 let pw = s.width as f32;
1319 let ph = s.height as f32;
1320 let now = std::time::Instant::now();
1321 let dt = now.duration_since(self.last_frame).as_secs_f32().min(0.1);
1322 self.last_frame = now;
1323 self.scene.clear();
1324 (self.draw_fn)(&mut self.pane, &mut self.scene, &mut self.input, pw, ph, dt);
1325 }
1326 }
1327 _ => {}
1328 }
1329 }
1330
1331 fn about_to_wait(&mut self, _: &ActiveEventLoop) {
1332 self.input.poll_gamepad();
1333 if let Some(win) = &self.window {
1334 win.request_redraw();
1335 }
1336 }
1337}
1338
1339pub fn run<F: FnMut(&mut Pane, &mut Scene, &mut Input, f32, f32, f32) + 'static>(draw_fn: F) {
1352 let el = EventLoop::new().unwrap();
1353 let mut app = App {
1354 window: None,
1355 pane: Pane::new_standalone(),
1356 scene: Scene::new(),
1357 input: Input::new_with_gilrs(),
1358 draw_fn,
1359 last_frame: std::time::Instant::now(),
1360 };
1361 el.run_app(&mut app).unwrap();
1362}