Skip to main content

bexa_ui_render/
lib.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use bexa_ui_core::{
5    build_taffy, clear_active_widgets, collect_focus_paths, dispatch_event, dispatch_scroll,
6    draw_widgets, handle_scrollbar_event, release_scrollbar_drag, sync_styles,
7    try_start_scrollbar_drag, update_widget_measures, widget_mut_at_path, Renderer, Theme,
8    WidgetNode, WindowRequest, WindowRequests,
9};
10use bytemuck::{Pod, Zeroable};
11use glyphon::{
12    Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
13    TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Weight,
14};
15use taffy::prelude::*;
16use wgpu::util::DeviceExt;
17use winit::event::{ElementState, Event, MouseButton, MouseScrollDelta, WindowEvent};
18use winit::event_loop::EventLoop;
19use winit::keyboard::{Key, ModifiersState, NamedKey};
20use winit::window::{Window, WindowBuilder, WindowId};
21
22#[repr(C)]
23#[derive(Copy, Clone, Pod, Zeroable)]
24struct Vertex {
25    position: [f32; 2],
26    uv: [f32; 2],
27    color: [f32; 4],
28    rect_center: [f32; 2],
29    rect_half: [f32; 2],
30    border_radius: f32,
31    border_width: f32,
32    border_color: [f32; 4],
33}
34
35impl Vertex {
36    fn layout() -> wgpu::VertexBufferLayout<'static> {
37        use std::mem::size_of;
38        wgpu::VertexBufferLayout {
39            array_stride: size_of::<Vertex>() as wgpu::BufferAddress,
40            step_mode: wgpu::VertexStepMode::Vertex,
41            attributes: &[
42                wgpu::VertexAttribute {
43                    offset: 0,
44                    shader_location: 0,
45                    format: wgpu::VertexFormat::Float32x2,
46                },
47                wgpu::VertexAttribute {
48                    offset: size_of::<[f32; 2]>() as u64,
49                    shader_location: 1,
50                    format: wgpu::VertexFormat::Float32x2,
51                },
52                wgpu::VertexAttribute {
53                    offset: size_of::<[f32; 4]>() as u64,
54                    shader_location: 2,
55                    format: wgpu::VertexFormat::Float32x4,
56                },
57                wgpu::VertexAttribute {
58                    offset: size_of::<[f32; 8]>() as u64,
59                    shader_location: 3,
60                    format: wgpu::VertexFormat::Float32x2,
61                },
62                wgpu::VertexAttribute {
63                    offset: size_of::<[f32; 10]>() as u64,
64                    shader_location: 4,
65                    format: wgpu::VertexFormat::Float32x2,
66                },
67                wgpu::VertexAttribute {
68                    offset: size_of::<[f32; 12]>() as u64,
69                    shader_location: 5,
70                    format: wgpu::VertexFormat::Float32,
71                },
72                wgpu::VertexAttribute {
73                    offset: size_of::<[f32; 13]>() as u64,
74                    shader_location: 6,
75                    format: wgpu::VertexFormat::Float32,
76                },
77                wgpu::VertexAttribute {
78                    offset: size_of::<[f32; 14]>() as u64,
79                    shader_location: 7,
80                    format: wgpu::VertexFormat::Float32x4,
81                },
82            ],
83        }
84    }
85}
86
87const SHADER_SRC: &str = r#"
88struct VertexOut {
89    @builtin(position) position: vec4<f32>,
90    @location(0) uv: vec2<f32>,
91    @location(1) color: vec4<f32>,
92    @location(2) rect_center: vec2<f32>,
93    @location(3) rect_half: vec2<f32>,
94    @location(4) border_radius: f32,
95    @location(5) border_width: f32,
96    @location(6) border_color: vec4<f32>,
97};
98
99@vertex
100fn vs_main(
101    @location(0) position: vec2<f32>,
102    @location(1) uv: vec2<f32>,
103    @location(2) color: vec4<f32>,
104    @location(3) rect_center: vec2<f32>,
105    @location(4) rect_half: vec2<f32>,
106    @location(5) border_radius: f32,
107    @location(6) border_width: f32,
108    @location(7) border_color: vec4<f32>,
109) -> VertexOut {
110    var out: VertexOut;
111    out.position = vec4<f32>(position, 0.0, 1.0);
112    out.uv = uv;
113    out.color = color;
114    out.rect_center = rect_center;
115    out.rect_half = rect_half;
116    out.border_radius = border_radius;
117    out.border_width = border_width;
118    out.border_color = border_color;
119    return out;
120}
121
122fn sdf_rounded_rect(p: vec2<f32>, half_size: vec2<f32>, radius: f32) -> f32 {
123    let r = min(radius, min(half_size.x, half_size.y));
124    let q = abs(p) - half_size + vec2<f32>(r, r);
125    return length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - r;
126}
127
128@fragment
129fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
130    let p = in.uv * in.rect_half;
131    let dist = sdf_rounded_rect(p, in.rect_half, in.border_radius);
132    let aa = 1.0;
133    let fill_alpha = 1.0 - smoothstep(-aa, 0.0, dist);
134
135    if fill_alpha < 0.001 {
136        discard;
137    }
138
139    var final_color = in.color;
140
141    if in.border_width > 0.0 {
142        let inner_dist = dist + in.border_width;
143        let border_mix = 1.0 - smoothstep(-aa, 0.0, inner_dist);
144        final_color = mix(in.border_color, in.color, border_mix);
145    }
146
147    final_color.a = final_color.a * fill_alpha;
148    return final_color;
149}
150"#;
151
152struct DrawBatch {
153    start: u32,
154    count: u32,
155    clip: Option<(f32, f32, f32, f32)>,
156}
157
158// ── Shared GPU resources (one per application) ──────────────────────────
159
160struct SharedGpu {
161    instance: wgpu::Instance,
162    device: wgpu::Device,
163    queue: wgpu::Queue,
164    render_pipeline: wgpu::RenderPipeline,
165    font_system: FontSystem,
166    swash_cache: SwashCache,
167    text_atlas: TextAtlas,
168    surface_format: wgpu::TextureFormat,
169}
170
171// ── Per-window state ────────────────────────────────────────────────────
172
173struct WindowState {
174    window: Arc<Window>,
175    surface: wgpu::Surface<'static>,
176    config: wgpu::SurfaceConfiguration,
177    size: winit::dpi::PhysicalSize<u32>,
178    // Rendering
179    vertex_buffer: wgpu::Buffer,
180    vertex_count: u32,
181    overlay_vertex_buffer: wgpu::Buffer,
182    overlay_vertex_count: u32,
183    draw_batches: Vec<DrawBatch>,
184    overlay_draw_batches: Vec<DrawBatch>,
185    text_renderer: TextRenderer,
186    overlay_text_renderer: TextRenderer,
187    text_viewport: Viewport,
188    text_buffers: Vec<Buffer>,
189    overlay_text_buffers: Vec<Buffer>,
190    // Widget tree
191    root: WidgetNode,
192    taffy: TaffyTree,
193    root_node: NodeId,
194    renderer: Renderer,
195    focus_paths: Vec<Vec<usize>>,
196    focused_index: Option<usize>,
197    modifiers: ModifiersState,
198    cursor_pos: (f32, f32),
199    theme: Theme,
200    is_main: bool,
201}
202
203impl WindowState {
204    fn new(
205        window: Arc<Window>,
206        mut root: WidgetNode,
207        theme: Theme,
208        gpu: &mut SharedGpu,
209        is_main: bool,
210    ) -> Self {
211        let size = window.inner_size();
212
213        let surface = gpu.instance
214            .create_surface(window.clone())
215            .expect("create surface");
216
217        let config = wgpu::SurfaceConfiguration {
218            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
219            format: gpu.surface_format,
220            width: size.width.max(1),
221            height: size.height.max(1),
222            present_mode: wgpu::PresentMode::Fifo,
223            alpha_mode: wgpu::CompositeAlphaMode::Auto,
224            view_formats: vec![],
225            desired_maximum_frame_latency: 2,
226        };
227        surface.configure(&gpu.device, &config);
228
229        let text_cache = Cache::new(&gpu.device);
230        let text_viewport = Viewport::new(&gpu.device, &text_cache);
231        let text_renderer = TextRenderer::new(
232            &mut gpu.text_atlas,
233            &gpu.device,
234            wgpu::MultisampleState::default(),
235            None,
236        );
237        let overlay_text_renderer = TextRenderer::new(
238            &mut gpu.text_atlas,
239            &gpu.device,
240            wgpu::MultisampleState::default(),
241            None,
242        );
243
244        let mut taffy = TaffyTree::new();
245        let root_node = build_taffy(&mut root, &mut taffy);
246        let mut focus_paths = Vec::new();
247        collect_focus_paths(&root, &mut Vec::new(), &mut focus_paths);
248
249        let vertex_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
250            label: Some("Quad Vertex Buffer"),
251            size: 4,
252            usage: wgpu::BufferUsages::VERTEX,
253            mapped_at_creation: false,
254        });
255        let overlay_vertex_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
256            label: Some("Overlay Vertex Buffer"),
257            size: 4,
258            usage: wgpu::BufferUsages::VERTEX,
259            mapped_at_creation: false,
260        });
261
262        let mut ws = Self {
263            window,
264            surface,
265            config,
266            size,
267            vertex_buffer,
268            vertex_count: 0,
269            overlay_vertex_buffer,
270            overlay_vertex_count: 0,
271            draw_batches: Vec::new(),
272            overlay_draw_batches: Vec::new(),
273            text_renderer,
274            overlay_text_renderer,
275            text_viewport,
276            text_buffers: Vec::new(),
277            overlay_text_buffers: Vec::new(),
278            root,
279            taffy,
280            root_node,
281            renderer: Renderer::new(),
282            focus_paths,
283            focused_index: None,
284            modifiers: ModifiersState::default(),
285            cursor_pos: (0.0, 0.0),
286            theme,
287            is_main,
288        };
289
290        if !ws.focus_paths.is_empty() {
291            ws.set_focus(Some(0));
292        }
293
294        ws
295    }
296
297    fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>, device: &wgpu::Device) {
298        if new_size.width == 0 || new_size.height == 0 {
299            return;
300        }
301        self.size = new_size;
302        self.config.width = new_size.width;
303        self.config.height = new_size.height;
304        self.surface.configure(device, &self.config);
305    }
306
307    fn update_layout(&mut self) {
308        let width = self.size.width as f32;
309        let height = self.size.height as f32;
310        if width == 0.0 || height == 0.0 {
311            return;
312        }
313        sync_styles(&mut self.root, &mut self.taffy, width, height, true);
314        let available_space = Size {
315            width: AvailableSpace::Definite(width),
316            height: AvailableSpace::Definite(height),
317        };
318        self.taffy
319            .compute_layout(self.root_node, available_space)
320            .expect("compute layout");
321    }
322
323    fn render(&mut self, gpu: &mut SharedGpu) -> Result<(), wgpu::SurfaceError> {
324        self.update_layout();
325
326        let viewport = (self.size.width as f32, self.size.height as f32);
327        self.renderer.clear();
328        draw_widgets(&self.root, &self.taffy, &mut self.renderer);
329
330        self.build_quad_vertices(viewport, &gpu.device);
331        self.build_overlay_vertices(viewport, &gpu.device);
332
333        self.text_viewport.update(
334            &gpu.queue,
335            Resolution {
336                width: self.config.width,
337                height: self.config.height,
338            },
339        );
340
341        let text_areas = build_text_areas(
342            &self.renderer.text_commands,
343            &mut self.text_buffers,
344            &mut gpu.font_system,
345            &mut self.renderer.text_measures,
346        );
347
348        update_widget_measures(&mut self.root, &self.renderer.text_measures);
349
350        self.text_renderer
351            .prepare(
352                &gpu.device,
353                &gpu.queue,
354                &mut gpu.font_system,
355                &mut gpu.text_atlas,
356                &self.text_viewport,
357                text_areas,
358                &mut gpu.swash_cache,
359            )
360            .expect("prepare text");
361
362        let output = self.surface.get_current_texture()?;
363        let view = output
364            .texture
365            .create_view(&wgpu::TextureViewDescriptor::default());
366
367        let mut encoder =
368            gpu.device
369                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
370                    label: Some("Render Encoder"),
371                });
372
373        {
374            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
375                label: Some("Render Pass"),
376                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
377                    view: &view,
378                    depth_slice: None,
379                    resolve_target: None,
380                    ops: wgpu::Operations {
381                        load: wgpu::LoadOp::Clear(wgpu::Color {
382                            r: self.theme.background[0] as f64,
383                            g: self.theme.background[1] as f64,
384                            b: self.theme.background[2] as f64,
385                            a: 1.0,
386                        }),
387                        store: wgpu::StoreOp::Store,
388                    },
389                })],
390                depth_stencil_attachment: None,
391                occlusion_query_set: None,
392                timestamp_writes: None,
393                multiview_mask: None,
394            });
395
396            let sw = self.size.width;
397            let sh = self.size.height;
398
399            // Pass 1: Main quads
400            render_pass.set_pipeline(&gpu.render_pipeline);
401            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
402            for batch in &self.draw_batches {
403                if let Some((cx, cy, cw, ch)) = batch.clip {
404                    let sx = (cx.max(0.0) as u32).min(sw);
405                    let sy = (cy.max(0.0) as u32).min(sh);
406                    let right = ((cx + cw).max(0.0) as u32).min(sw);
407                    let bottom = ((cy + ch).max(0.0) as u32).min(sh);
408                    let swidth = right.saturating_sub(sx);
409                    let sheight = bottom.saturating_sub(sy);
410                    if swidth == 0 || sheight == 0 {
411                        continue;
412                    }
413                    render_pass.set_scissor_rect(sx, sy, swidth, sheight);
414                } else {
415                    render_pass.set_scissor_rect(0, 0, sw, sh);
416                }
417                render_pass.draw(batch.start..batch.start + batch.count, 0..1);
418            }
419
420            // Pass 2: Main text
421            render_pass.set_scissor_rect(0, 0, sw, sh);
422            self.text_renderer
423                .render(&gpu.text_atlas, &self.text_viewport, &mut render_pass)
424                .expect("render text");
425
426            // Pass 3: Overlay quads
427            if self.overlay_vertex_count > 0 {
428                render_pass.set_pipeline(&gpu.render_pipeline);
429                render_pass.set_vertex_buffer(0, self.overlay_vertex_buffer.slice(..));
430                for batch in &self.overlay_draw_batches {
431                    if let Some((cx, cy, cw, ch)) = batch.clip {
432                        let sx = (cx.max(0.0) as u32).min(sw);
433                        let sy = (cy.max(0.0) as u32).min(sh);
434                        let right = ((cx + cw).max(0.0) as u32).min(sw);
435                        let bottom = ((cy + ch).max(0.0) as u32).min(sh);
436                        let swidth = right.saturating_sub(sx);
437                        let sheight = bottom.saturating_sub(sy);
438                        if swidth == 0 || sheight == 0 {
439                            continue;
440                        }
441                        render_pass.set_scissor_rect(sx, sy, swidth, sheight);
442                    } else {
443                        render_pass.set_scissor_rect(0, 0, sw, sh);
444                    }
445                    render_pass.draw(batch.start..batch.start + batch.count, 0..1);
446                }
447            }
448        }
449
450        // Pass 4: Overlay text
451        if !self.renderer.overlay_text_commands.is_empty() {
452            let overlay_text_areas = build_text_areas(
453                &self.renderer.overlay_text_commands,
454                &mut self.overlay_text_buffers,
455                &mut gpu.font_system,
456                &mut vec![],
457            );
458
459            self.overlay_text_renderer
460                .prepare(
461                    &gpu.device,
462                    &gpu.queue,
463                    &mut gpu.font_system,
464                    &mut gpu.text_atlas,
465                    &self.text_viewport,
466                    overlay_text_areas,
467                    &mut gpu.swash_cache,
468                )
469                .expect("prepare overlay text");
470
471            {
472                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
473                    label: Some("Overlay Text Pass"),
474                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
475                        view: &view,
476                        depth_slice: None,
477                        resolve_target: None,
478                        ops: wgpu::Operations {
479                            load: wgpu::LoadOp::Load,
480                            store: wgpu::StoreOp::Store,
481                        },
482                    })],
483                    depth_stencil_attachment: None,
484                    occlusion_query_set: None,
485                    timestamp_writes: None,
486                    multiview_mask: None,
487                });
488
489                let sw = self.size.width;
490                let sh = self.size.height;
491                render_pass.set_scissor_rect(0, 0, sw, sh);
492                self.overlay_text_renderer
493                    .render(&gpu.text_atlas, &self.text_viewport, &mut render_pass)
494                    .expect("render overlay text");
495            }
496        }
497
498        gpu.queue.submit(Some(encoder.finish()));
499        output.present();
500        gpu.text_atlas.trim();
501
502        Ok(())
503    }
504
505    fn handle_window_event(&mut self, event: &WindowEvent) {
506        if let WindowEvent::CursorMoved { position, .. } = event {
507            self.cursor_pos = (position.x as f32, position.y as f32);
508        }
509
510        if handle_scrollbar_event(&mut self.root, &self.taffy, event) {
511            return;
512        }
513
514        if let WindowEvent::MouseInput {
515            state: ElementState::Pressed,
516            button: MouseButton::Left,
517            ..
518        } = event
519        {
520            let (cx, cy) = self.cursor_pos;
521            if try_start_scrollbar_drag(&mut self.root, &self.taffy, cx, cy) {
522                return;
523            }
524        }
525
526        if let WindowEvent::MouseInput {
527            state: ElementState::Released,
528            button: MouseButton::Left,
529            ..
530        } = event
531        {
532            release_scrollbar_drag(&mut self.root);
533        }
534
535        let mut path = Vec::new();
536        if let Some(consumed_path) =
537            dispatch_event(&mut self.root, &self.taffy, event, &mut path)
538        {
539            if matches!(
540                event,
541                WindowEvent::MouseInput {
542                    state: ElementState::Pressed,
543                    button: MouseButton::Left,
544                    ..
545                }
546            ) {
547                self.set_focus_by_path(&consumed_path);
548            }
549        }
550    }
551
552    fn handle_mouse_wheel(&mut self, delta: MouseScrollDelta) {
553        let delta_y = match delta {
554            MouseScrollDelta::LineDelta(_, y) => y * 40.0,
555            MouseScrollDelta::PixelDelta(d) => d.y as f32,
556        };
557        let (cx, cy) = self.cursor_pos;
558        dispatch_scroll(&mut self.root, delta_y, cx, cy, &self.taffy);
559    }
560
561    fn handle_keyboard_input(&mut self, event: &winit::event::KeyEvent) {
562        if let Some(idx) = self.focused_index {
563            if let Some(path) = self.focus_paths.get(idx).cloned() {
564                if let Some(widget) = widget_mut_at_path(&mut self.root, &path) {
565                    if widget.handle_key_event(event, self.modifiers) {
566                        return;
567                    }
568                }
569            }
570        }
571
572        match &event.logical_key {
573            Key::Named(NamedKey::Tab) => {
574                let reverse = self.modifiers.shift_key();
575                self.focus_next(reverse);
576            }
577            Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
578                self.activate_focused();
579            }
580            Key::Named(NamedKey::Escape) => {
581                self.clear_active();
582            }
583            _ => {}
584        }
585    }
586
587    fn focus_next(&mut self, reverse: bool) {
588        if self.focus_paths.is_empty() {
589            return;
590        }
591        let count = self.focus_paths.len();
592        let current = self.focused_index.unwrap_or(0);
593        let next = if reverse {
594            (current + count - 1) % count
595        } else {
596            (current + 1) % count
597        };
598        self.set_focus(Some(next));
599    }
600
601    fn activate_focused(&mut self) {
602        let Some(index) = self.focused_index else {
603            return;
604        };
605        if let Some(path) = self.focus_paths.get(index).cloned() {
606            if let Some(widget) = widget_mut_at_path(&mut self.root, &path) {
607                widget.activate();
608            }
609        }
610    }
611
612    fn clear_active(&mut self) {
613        clear_active_widgets(&mut self.root);
614    }
615
616    fn set_focus(&mut self, index: Option<usize>) {
617        self.focused_index = index;
618        for (i, path) in self.focus_paths.iter().enumerate() {
619            if let Some(widget) = widget_mut_at_path(&mut self.root, path) {
620                widget.set_focus(Some(i) == index);
621            }
622        }
623    }
624
625    fn set_focus_by_path(&mut self, path: &[usize]) {
626        if let Some(index) = self.focus_paths.iter().position(|p| p == path) {
627            self.set_focus(Some(index));
628        }
629    }
630
631    fn build_quad_vertices(&mut self, viewport: (f32, f32), device: &wgpu::Device) {
632        let mut vertices = Vec::with_capacity(self.renderer.quad_commands.len() * 6);
633        let (vw, vh) = viewport;
634
635        self.draw_batches.clear();
636        let mut current_clip: Option<(f32, f32, f32, f32)> = None;
637        let mut batch_start: u32 = 0;
638
639        for cmd in &self.renderer.quad_commands {
640            if cmd.clip != current_clip {
641                let vert_count = vertices.len() as u32;
642                if vert_count > batch_start {
643                    self.draw_batches.push(DrawBatch {
644                        start: batch_start,
645                        count: vert_count - batch_start,
646                        clip: current_clip,
647                    });
648                }
649                current_clip = cmd.clip;
650                batch_start = vert_count;
651            }
652
653            let (x, y, w, h) = cmd.rect;
654            let x0 = (x / vw) * 2.0 - 1.0;
655            let x1 = ((x + w) / vw) * 2.0 - 1.0;
656            let y0 = 1.0 - (y / vh) * 2.0;
657            let y1 = 1.0 - ((y + h) / vh) * 2.0;
658            let cx = x + w * 0.5;
659            let cy = y + h * 0.5;
660            let hx = w * 0.5;
661            let hy = h * 0.5;
662
663            let make_vertex = |px: f32, py: f32, u: f32, v: f32| Vertex {
664                position: [px, py],
665                uv: [u, v],
666                color: cmd.color,
667                rect_center: [cx, cy],
668                rect_half: [hx, hy],
669                border_radius: cmd.border_radius,
670                border_width: cmd.border_width,
671                border_color: cmd.border_color,
672            };
673
674            vertices.push(make_vertex(x0, y1, -1.0, 1.0));
675            vertices.push(make_vertex(x1, y1, 1.0, 1.0));
676            vertices.push(make_vertex(x1, y0, 1.0, -1.0));
677            vertices.push(make_vertex(x0, y1, -1.0, 1.0));
678            vertices.push(make_vertex(x1, y0, 1.0, -1.0));
679            vertices.push(make_vertex(x0, y0, -1.0, -1.0));
680        }
681
682        let vert_count = vertices.len() as u32;
683        if vert_count > batch_start {
684            self.draw_batches.push(DrawBatch {
685                start: batch_start,
686                count: vert_count - batch_start,
687                clip: current_clip,
688            });
689        }
690
691        self.vertex_count = vertices.len() as u32;
692        if vertices.is_empty() {
693            self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
694                label: Some("Quad Vertex Buffer"),
695                size: 4,
696                usage: wgpu::BufferUsages::VERTEX,
697                mapped_at_creation: false,
698            });
699        } else {
700            self.vertex_buffer =
701                device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
702                    label: Some("Quad Vertex Buffer"),
703                    contents: bytemuck::cast_slice(&vertices),
704                    usage: wgpu::BufferUsages::VERTEX,
705                });
706        }
707    }
708
709    fn build_overlay_vertices(&mut self, viewport: (f32, f32), device: &wgpu::Device) {
710        let mut vertices =
711            Vec::with_capacity(self.renderer.overlay_quad_commands.len() * 6);
712        let (vw, vh) = viewport;
713
714        self.overlay_draw_batches.clear();
715        let mut current_clip: Option<(f32, f32, f32, f32)> = None;
716        let mut batch_start: u32 = 0;
717
718        for cmd in &self.renderer.overlay_quad_commands {
719            if cmd.clip != current_clip {
720                let vert_count = vertices.len() as u32;
721                if vert_count > batch_start {
722                    self.overlay_draw_batches.push(DrawBatch {
723                        start: batch_start,
724                        count: vert_count - batch_start,
725                        clip: current_clip,
726                    });
727                }
728                current_clip = cmd.clip;
729                batch_start = vert_count;
730            }
731
732            let (x, y, w, h) = cmd.rect;
733            let x0 = (x / vw) * 2.0 - 1.0;
734            let x1 = ((x + w) / vw) * 2.0 - 1.0;
735            let y0 = 1.0 - (y / vh) * 2.0;
736            let y1 = 1.0 - ((y + h) / vh) * 2.0;
737            let cx = x + w * 0.5;
738            let cy = y + h * 0.5;
739            let hx = w * 0.5;
740            let hy = h * 0.5;
741
742            let make_vertex = |px: f32, py: f32, u: f32, v: f32| Vertex {
743                position: [px, py],
744                uv: [u, v],
745                color: cmd.color,
746                rect_center: [cx, cy],
747                rect_half: [hx, hy],
748                border_radius: cmd.border_radius,
749                border_width: cmd.border_width,
750                border_color: cmd.border_color,
751            };
752
753            vertices.push(make_vertex(x0, y1, -1.0, 1.0));
754            vertices.push(make_vertex(x1, y1, 1.0, 1.0));
755            vertices.push(make_vertex(x1, y0, 1.0, -1.0));
756            vertices.push(make_vertex(x0, y1, -1.0, 1.0));
757            vertices.push(make_vertex(x1, y0, 1.0, -1.0));
758            vertices.push(make_vertex(x0, y0, -1.0, -1.0));
759        }
760
761        let vert_count = vertices.len() as u32;
762        if vert_count > batch_start {
763            self.overlay_draw_batches.push(DrawBatch {
764                start: batch_start,
765                count: vert_count - batch_start,
766                clip: current_clip,
767            });
768        }
769
770        self.overlay_vertex_count = vertices.len() as u32;
771        if vertices.is_empty() {
772            self.overlay_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
773                label: Some("Overlay Vertex Buffer"),
774                size: 4,
775                usage: wgpu::BufferUsages::VERTEX,
776                mapped_at_creation: false,
777            });
778        } else {
779            self.overlay_vertex_buffer =
780                device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
781                    label: Some("Overlay Vertex Buffer"),
782                    contents: bytemuck::cast_slice(&vertices),
783                    usage: wgpu::BufferUsages::VERTEX,
784                });
785        }
786    }
787}
788
789// ── App (public API) ────────────────────────────────────────────────────
790
791pub struct App {
792    root: WidgetNode,
793    theme: Theme,
794    title: String,
795    window_requests: Option<WindowRequests>,
796}
797
798impl App {
799    pub fn new(root: WidgetNode) -> Self {
800        Self {
801            root,
802            theme: Theme::ocean(),
803            title: "BexaUI".to_string(),
804            window_requests: None,
805        }
806    }
807
808    pub fn theme(mut self, theme: Theme) -> Self {
809        self.theme = theme;
810        self
811    }
812
813    pub fn title(mut self, title: impl Into<String>) -> Self {
814        self.title = title.into();
815        self
816    }
817
818    pub fn with_requests(mut self, requests: WindowRequests) -> Self {
819        self.window_requests = Some(requests);
820        self
821    }
822
823    /// Create a shared `WindowRequests` handle for widgets to request new windows.
824    pub fn window_requests() -> WindowRequests {
825        bexa_ui_core::create_window_requests()
826    }
827
828    pub fn run(self) {
829        let event_loop = EventLoop::new().expect("create event loop");
830
831        // Create initial window
832        let window = Arc::new(
833            WindowBuilder::new()
834                .with_title(&self.title)
835                .build(&event_loop)
836                .expect("create window"),
837        );
838
839        // Initialize shared GPU resources
840        let mut gpu = pollster::block_on(init_gpu(window.clone()));
841
842        // Create main window state
843        let main_ws = WindowState::new(window.clone(), self.root, self.theme, &mut gpu, true);
844        let main_id = main_ws.window.id();
845
846        let mut windows: HashMap<WindowId, WindowState> = HashMap::new();
847        windows.insert(main_id, main_ws);
848
849        let window_requests = self.window_requests;
850
851        event_loop
852            .run(move |event, elwt| {
853                elwt.set_control_flow(winit::event_loop::ControlFlow::Poll);
854                match event {
855                Event::WindowEvent {
856                    event: ref win_event,
857                    window_id,
858                } => {
859                    if let Some(ws) = windows.get_mut(&window_id) {
860                        match win_event {
861                            WindowEvent::CloseRequested => {
862                                if ws.is_main {
863                                    elwt.exit();
864                                } else {
865                                    windows.remove(&window_id);
866                                }
867                            }
868                            WindowEvent::Resized(size) => {
869                                ws.resize(*size, &gpu.device);
870                            }
871                            WindowEvent::CursorMoved { .. }
872                            | WindowEvent::MouseInput { .. } => {
873                                ws.handle_window_event(win_event);
874                            }
875                            WindowEvent::MouseWheel { delta, .. } => {
876                                ws.handle_mouse_wheel(*delta);
877                            }
878                            WindowEvent::KeyboardInput { event, .. } => {
879                                if event.state == ElementState::Pressed {
880                                    ws.handle_keyboard_input(event);
881                                }
882                            }
883                            WindowEvent::ModifiersChanged(modifiers) => {
884                                ws.modifiers = modifiers.state();
885                            }
886                            WindowEvent::RedrawRequested => {
887                                match ws.render(&mut gpu) {
888                                    Ok(()) => {}
889                                    Err(wgpu::SurfaceError::Lost) => {
890                                        let size = ws.size;
891                                        ws.resize(size, &gpu.device);
892                                    }
893                                    Err(wgpu::SurfaceError::OutOfMemory) => elwt.exit(),
894                                    Err(_) => {}
895                                }
896                            }
897                            _ => {}
898                        }
899                    }
900                }
901                Event::AboutToWait => {
902                    // Process pending window creation requests
903                    if let Some(ref reqs) = window_requests {
904                        let pending: Vec<WindowRequest> = {
905                            let mut lock = reqs.lock().unwrap();
906                            lock.drain(..).collect()
907                        };
908                        for req in pending {
909                            let new_window = Arc::new(
910                                WindowBuilder::new()
911                                    .with_title(&req.title)
912                                    .with_inner_size(winit::dpi::LogicalSize::new(
913                                        req.width, req.height,
914                                    ))
915                                    .build(elwt)
916                                    .expect("create child window"),
917                            );
918                            let new_id = new_window.id();
919                            let ws = WindowState::new(
920                                new_window,
921                                req.root,
922                                req.theme,
923                                &mut gpu,
924                                false,
925                            );
926                            windows.insert(new_id, ws);
927                        }
928                    }
929
930                    // Request redraw for all windows
931                    for ws in windows.values() {
932                        ws.window.request_redraw();
933                    }
934                }
935                _ => {}
936            }})
937            .expect("run event loop");
938    }
939}
940
941// ── GPU Initialization ──────────────────────────────────────────────────
942
943async fn init_gpu(window: Arc<Window>) -> SharedGpu {
944    let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
945        backends: wgpu::Backends::PRIMARY,
946        ..Default::default()
947    });
948
949    let surface = instance
950        .create_surface(window.clone())
951        .expect("create surface");
952    let adapter = instance
953        .request_adapter(&wgpu::RequestAdapterOptions {
954            power_preference: wgpu::PowerPreference::HighPerformance,
955            compatible_surface: Some(&surface),
956            force_fallback_adapter: false,
957        })
958        .await
959        .expect("find GPU adapter");
960
961    let (device, queue) = adapter
962        .request_device(&wgpu::DeviceDescriptor {
963            label: None,
964            required_features: wgpu::Features::empty(),
965            required_limits: wgpu::Limits::default(),
966            ..Default::default()
967        })
968        .await
969        .expect("create device");
970
971    let surface_caps = surface.get_capabilities(&adapter);
972    let surface_format = surface_caps
973        .formats
974        .iter()
975        .copied()
976        .find(|format| format.is_srgb())
977        .unwrap_or(surface_caps.formats[0]);
978
979    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
980        label: Some("Quad SDF Shader"),
981        source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
982    });
983
984    let render_pipeline_layout =
985        device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
986            label: Some("Quad Pipeline Layout"),
987            bind_group_layouts: &[],
988            immediate_size: 0,
989        });
990
991    let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
992        label: Some("Quad Pipeline"),
993        layout: Some(&render_pipeline_layout),
994        vertex: wgpu::VertexState {
995            module: &shader,
996            entry_point: Some("vs_main"),
997            buffers: &[Vertex::layout()],
998            compilation_options: Default::default(),
999        },
1000        fragment: Some(wgpu::FragmentState {
1001            module: &shader,
1002            entry_point: Some("fs_main"),
1003            targets: &[Some(wgpu::ColorTargetState {
1004                format: surface_format,
1005                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1006                write_mask: wgpu::ColorWrites::ALL,
1007            })],
1008            compilation_options: Default::default(),
1009        }),
1010        primitive: wgpu::PrimitiveState {
1011            topology: wgpu::PrimitiveTopology::TriangleList,
1012            strip_index_format: None,
1013            front_face: wgpu::FrontFace::Ccw,
1014            cull_mode: None,
1015            ..Default::default()
1016        },
1017        depth_stencil: None,
1018        multisample: wgpu::MultisampleState::default(),
1019        multiview_mask: None,
1020        cache: None,
1021    });
1022
1023    let mut font_system = FontSystem::new();
1024    let nerd_font_data = include_bytes!("../assets/fonts/SymbolsNerdFont-Regular.ttf");
1025    font_system
1026        .db_mut()
1027        .load_font_data(nerd_font_data.to_vec());
1028    let swash_cache = SwashCache::new();
1029    let text_cache = Cache::new(&device);
1030    let _text_viewport = Viewport::new(&device, &text_cache);
1031    let text_atlas = TextAtlas::new(&device, &queue, &text_cache, surface_format);
1032
1033    SharedGpu {
1034        instance,
1035        device,
1036        queue,
1037        render_pipeline,
1038        font_system,
1039        swash_cache,
1040        text_atlas,
1041        surface_format,
1042    }
1043}
1044
1045// ── Text area builder (shared) ──────────────────────────────────────────
1046
1047fn build_text_areas<'a>(
1048    commands: &'a [bexa_ui_core::TextCommand],
1049    text_buffers: &'a mut Vec<Buffer>,
1050    font_system: &mut FontSystem,
1051    measures_out: &mut Vec<Vec<f32>>,
1052) -> Vec<TextArea<'a>> {
1053    let mut areas = Vec::with_capacity(commands.len());
1054    measures_out.clear();
1055    measures_out.resize(commands.len(), vec![]);
1056
1057    if text_buffers.len() < commands.len() {
1058        for _ in text_buffers.len()..commands.len() {
1059            text_buffers.push(Buffer::new(font_system, Metrics::new(16.0, 20.0)));
1060        }
1061    }
1062
1063    for (idx, command) in commands.iter().enumerate() {
1064        let buffer = &mut text_buffers[idx];
1065        *buffer = Buffer::new(font_system, command.metrics);
1066
1067        let family = match &command.font_family {
1068            Some(name) => Family::Name(name),
1069            None => Family::SansSerif,
1070        };
1071        let attrs = Attrs::new().family(family).weight(Weight::MEDIUM);
1072
1073        buffer.set_text(
1074            font_system,
1075            &command.text,
1076            &attrs,
1077            Shaping::Advanced,
1078            Some(command.align),
1079        );
1080        buffer.set_size(
1081            font_system,
1082            Some(command.bounds.0),
1083            Some(command.bounds.1),
1084        );
1085        buffer.shape_until_scroll(font_system, false);
1086
1087        if !command.measure_chars.is_empty() {
1088            let mut results = Vec::with_capacity(command.measure_chars.len());
1089            for &char_pos in &command.measure_chars {
1090                let byte_pos = command
1091                    .text
1092                    .char_indices()
1093                    .nth(char_pos)
1094                    .map(|(i, _)| i)
1095                    .unwrap_or(command.text.len());
1096
1097                let mut width = 0.0f32;
1098                for run in buffer.layout_runs() {
1099                    for glyph in run.glyphs.iter() {
1100                        if glyph.start < byte_pos {
1101                            width = glyph.x + glyph.w;
1102                        }
1103                    }
1104                }
1105                results.push(width);
1106            }
1107            measures_out[idx] = results;
1108        }
1109    }
1110
1111    for (idx, command) in commands.iter().enumerate() {
1112        let buffer = &text_buffers[idx];
1113
1114        let mut left = command.pos.0 as i32;
1115        let mut top = command.pos.1 as i32;
1116        let mut right = (command.pos.0 + command.bounds.0) as i32;
1117        let mut bottom = (command.pos.1 + command.bounds.1) as i32;
1118
1119        if let Some((cx, cy, cw, ch)) = command.clip {
1120            left = left.max(cx as i32);
1121            top = top.max(cy as i32);
1122            right = right.min((cx + cw) as i32);
1123            bottom = bottom.min((cy + ch) as i32);
1124        }
1125
1126        if right <= left || bottom <= top {
1127            continue;
1128        }
1129
1130        areas.push(TextArea {
1131            buffer,
1132            left: command.pos.0,
1133            top: command.pos.1,
1134            scale: 1.0,
1135            bounds: TextBounds {
1136                left,
1137                top,
1138                right,
1139                bottom,
1140            },
1141            default_color: Color::rgb(command.color[0], command.color[1], command.color[2]),
1142            custom_glyphs: &[],
1143        });
1144    }
1145
1146    areas
1147}