keru/
ui.rs

1use crate::*;
2
3use crate::math::Axis::*;
4
5use arboard::Clipboard;
6use basic_window_loop::basic_depth_stencil_state;
7use glam::DVec2;
8use glyphon::Cache as GlyphonCache;
9use glyphon::Viewport;
10
11use slab::Slab;
12use wgpu::{
13    BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry,
14    BindingResource, BindingType, BlendState, Buffer, BufferBindingType, ColorWrites, FilterMode,
15    FragmentState, PipelineLayoutDescriptor, PrimitiveState, RenderPipelineDescriptor,
16    SamplerBindingType, SamplerDescriptor, ShaderModuleDescriptor, ShaderSource, ShaderStages,
17    TextureSampleType, TextureViewDimension, VertexState,
18};
19use winit::dpi::PhysicalSize;
20use winit_key_events::KeyInput;
21use winit_mouse_events::MouseInput;
22
23use std::sync::atomic::AtomicU64;
24use std::sync::atomic::Ordering;
25use std::sync::LazyLock;
26use std::time::Duration;
27use std::{mem, time::Instant};
28
29use bytemuck::{Pod, Zeroable};
30use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
31use wgpu::{
32    util::{self, DeviceExt},
33    BindGroup, BufferAddress, BufferUsages, ColorTargetState, Device, MultisampleState, Queue,
34    RenderPipeline, SurfaceConfiguration, VertexBufferLayout, VertexStepMode,
35};
36
37pub(crate) static T0: LazyLock<Instant> = LazyLock::new(Instant::now);
38
39pub(crate) fn ui_time_f32() -> f32 {
40    return T0.elapsed().as_secs_f32();
41}
42
43/// The central struct of the library, representing the whole GUI state.
44/// 
45/// To create a new [`Ui`] instance, use [`Ui::new`].
46/// 
47/// To build a GUI, add nodes to the [`Ui`] by calling [`Ui::add`].
48/// 
49/// To react to mouse clicks and other node events, call [`Ui::is_clicked`] and similar methods.
50/// 
51/// To integrate [`Ui`] with your `winit` event loop, pass all your `winit` events to [`Ui::window_event`].
52/// 
53/// To render the GUI to the screen, call [`Ui::render`]. 
54pub struct Ui {
55    pub(crate) nodes: Nodes,
56    pub(crate) sys: System,
57    pub(crate) format_scratch: String,
58}
59
60static INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(1);
61
62pub(crate) struct System {
63    // in inspect mode, draw invisible rects as well, for example V_STACKs.
64    // usually these have filled = false (just the outline), but this is not enforced.
65    pub inspect_mode: bool,
66
67    pub unique_id: u64,
68    pub theme: Theme,
69    pub debug_key_pressed: bool,
70
71    pub new_ui_input: u8,
72    pub new_external_events: bool,
73
74    pub clipboard: Clipboard,
75
76    pub gpu_rect_buffer: TypedGpuBuffer<RenderRect>,
77    pub render_pipeline: RenderPipeline,
78
79    pub base_uniform_buffer: Buffer,
80    pub bind_group: BindGroup,
81
82    pub text: TextSystem,
83    pub texture_atlas: TextureAtlas,
84
85    pub z_cursor: f32,
86    pub rects: Vec<RenderRect>,
87    pub editor_rects_i: u16,
88
89    pub click_rects: Vec<ClickRect>,
90
91    pub scroll_rects: Vec<ClickRect>,
92
93    pub unifs: Uniforms,
94    pub current_frame: u64,
95    pub last_frame_end_fake_time: u64,
96    pub second_last_frame_end_fake_time: u64,
97    pub third_last_frame_end_fake_time: u64,
98
99    pub mouse_hit_stack: Vec<(Id, f32)>,
100
101    // mouse input needs to be Id based, not NodeI based, because you can hold a button for several frames
102    pub mouse_input: MouseInput<Id>,
103    pub key_input: KeyInput,
104
105    #[cfg(debug_assertions)]
106    pub inspect_hovered: Option<Id>,
107
108    pub hovered: Vec<Id>,
109    pub hovered_scroll_area: Option<Id>,
110
111    pub focused: Option<Id>,
112
113    // this is used exclusively for info messages
114    pub partial_relayout_count: u32,
115
116    pub old_child_collect: Vec<NodeI>,
117    pub new_child_collect: Vec<NodeI>,
118    pub added_nodes: Vec<NodeI>,
119    // nodes that were removed and were direct children of still-visible nodes. Among other things, this means that them disappearing has to trigger a partial relayout.
120    pub direct_removed_nodes: Vec<NodeI>,
121    // nodes that were removed "automatically" as a consequence of their parent or grandparent being directly removed. Aka orphaned nodes. These ones don't cause relayouts.
122    pub indirect_removed_nodes: Vec<NodeI>,
123    // nodes that were excluded from the tree, but stay hidden. still have to do relayouts for them.
124    pub hidden_nodes: Vec<NodeI>,
125
126    // There are also nodes that are hidden automatically due to their parent or grandparent being hidden. These don't cause relayouts. They don't get garbage collected immediately, but they do have to be garbage collected later if the root of the hidden branch gets garbage collected.
127    // While hidden, they stay connected to each other automatically with the regular tree links. But they need to be connected to the hidden root with the hidden tree links.
128    // And if the hidden root gets garbage collected, it needs to go through its hidden children and GC all their *regular* children, and then GC them.
129    // All the removals from there are collected here
130    pub very_indirect_removed_nodes: Vec<NodeI>,
131
132
133    pub changes: PartialChanges,
134
135    // move to changes oalgo
136    pub anim_render_timer: AnimationRenderTimer,
137
138    // todo: probably remove
139    pub hidden_stack: Vec<NodeI>,
140}
141
142pub(crate) struct AnimationRenderTimer(Option<Instant>);
143
144impl AnimationRenderTimer {
145    fn default() -> Self {
146        Self(None)
147    }
148
149    pub(crate) fn push_new(&mut self, duration: Duration) {
150        let now = Instant::now();
151        let new_end = now + duration;
152
153        if let Some(end) = self.0 {
154            if new_end > end {
155                *self = AnimationRenderTimer(Some(new_end));
156            }
157        } else {
158            *self = AnimationRenderTimer(Some(new_end));
159        }
160    }
161
162    pub(crate) fn is_live(&mut self) -> bool {
163        if let Some(end) = self.0 {
164            let is_live = Instant::now() < end;
165            if !is_live {
166                *self = AnimationRenderTimer(None);
167            }
168            return is_live;
169        }
170        false
171    }
172}
173
174#[repr(C)]
175#[derive(Debug, Pod, Copy, Clone, Zeroable)]
176pub(crate) struct Uniforms {
177    pub size: Xy<f32>,
178    pub t: f32,
179    pub _padding: f32,
180}
181
182impl Ui {
183    pub fn new(device: &Device, queue: &Queue, config: &SurfaceConfiguration) -> Self {
184        // initialize the static T0
185        LazyLock::force(&T0);
186        
187        let gpu_rect_buffer = device.create_buffer_init(&util::BufferInitDescriptor {
188            label: Some("Keru rectangle buffer"),
189            // todo: I guess this should be growable
190            contents: {
191                bytemuck::cast_slice(&[0.0; 2048])
192            },
193            usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
194        });
195
196        let gpu_rect_buffer = TypedGpuBuffer::new(gpu_rect_buffer);
197        let vert_buff_layout = VertexBufferLayout {
198            array_stride: mem::size_of::<RenderRect>() as BufferAddress,
199            step_mode: VertexStepMode::Instance,
200            attributes: &RenderRect::buffer_desc(),
201        };
202
203        let uniforms = Uniforms {
204            size: Xy::new(config.width as f32, config.height as f32),
205            t: 0.,
206            _padding: 0.,
207        };
208        let resolution_buffer = device.create_buffer_init(&util::BufferInitDescriptor {
209            label: Some("Resolution Uniform Buffer"),
210            contents: bytemuck::bytes_of(&uniforms),
211            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
212        });
213
214        let mut texture_atlas = TextureAtlas::new(device);
215
216        let _white_alloc = texture_atlas.allocate_image(include_bytes!("textures/white.png"));
217        // let _debug_alloc = texture_atlas.allocate_image(include_bytes!("textures/debug.png"));
218
219        let texture_sampler = device.create_sampler(&SamplerDescriptor {
220            label: Some("Texture sampler"),
221            min_filter: FilterMode::Nearest,
222            mag_filter: FilterMode::Nearest,
223            mipmap_filter: FilterMode::Nearest,
224            lod_min_clamp: 0f32,
225            lod_max_clamp: 0f32,
226            ..Default::default()
227        });
228
229        let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
230            entries: &[
231                BindGroupLayoutEntry {
232                    binding: 0,
233                    visibility: ShaderStages::VERTEX,
234                    ty: BindingType::Buffer {
235                        ty: BufferBindingType::Uniform,
236                        has_dynamic_offset: false,
237                        min_binding_size: None,
238                    },
239                    count: None,
240                },
241                BindGroupLayoutEntry {
242                    binding: 1,
243                    visibility: ShaderStages::FRAGMENT,
244                    ty: BindingType::Texture {
245                        multisampled: false,
246                        view_dimension: TextureViewDimension::D2,
247                        sample_type: TextureSampleType::Float { filterable: true },
248                    },
249                    count: None,
250                },
251                BindGroupLayoutEntry {
252                    binding: 2,
253                    visibility: ShaderStages::FRAGMENT,
254                    ty: BindingType::Sampler(SamplerBindingType::Filtering),
255                    count: None,
256                },
257            ],
258            label: Some("Keru Bind Group Layout"),
259        });
260
261        // Create the bind group
262        let bind_group = device.create_bind_group(&BindGroupDescriptor {
263            layout: &bind_group_layout,
264            entries: &[
265                BindGroupEntry {
266                    binding: 0,
267                    resource: resolution_buffer.as_entire_binding(),
268                },
269                BindGroupEntry {
270                    binding: 1,
271                    resource: BindingResource::TextureView(texture_atlas.texture_view()),
272                },
273                BindGroupEntry {
274                    binding: 2,
275                    resource: BindingResource::Sampler(&texture_sampler),
276                },
277            ],
278            label: Some("Keru Bind Group"),
279        });
280
281        let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
282            label: None,
283            bind_group_layouts: &[&bind_group_layout],
284            push_constant_ranges: &[],
285        });
286
287        let shader = device.create_shader_module(ShaderModuleDescriptor {
288            label: None,
289            source: ShaderSource::Wgsl(include_str!("shaders/box.wgsl").into()),
290        });
291
292        let primitive = PrimitiveState::default();
293
294        let render_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
295            label: None,
296            layout: Some(&pipeline_layout),
297            vertex: VertexState {
298                module: &shader,
299                entry_point: Some("vs_main"),
300                buffers: &[vert_buff_layout],
301                compilation_options: Default::default(),
302            },
303            fragment: Some(FragmentState {
304                module: &shader,
305                entry_point: Some("fs_main"),
306                targets: &[Some(ColorTargetState {
307                    format: config.format,
308                    blend: Some(BlendState::ALPHA_BLENDING),
309                    write_mask: ColorWrites::ALL,
310                })],
311                compilation_options: Default::default(),
312            }),
313            primitive,
314            depth_stencil: Some(basic_depth_stencil_state()),
315            multisample: MultisampleState::default(),
316            multiview: None,
317            cache: None,
318        });
319
320        let font_system = FontSystem::new();
321        let cache = SwashCache::new();
322        let glyphon_cache = GlyphonCache::new(device);
323        let glyphon_viewport = Viewport::new(device, &glyphon_cache);
324
325        let mut atlas = TextAtlas::new(device, queue, &glyphon_cache, config.format);
326        let text_renderer =
327            TextRenderer::new(&mut atlas, device, MultisampleState::default(), Some(basic_depth_stencil_state()));
328
329        let nodes = Nodes::new();
330
331        let third_last_frame_end_fake_time = 3;
332        let second_last_frame_end_fake_time = 4;
333        let last_frame_end_fake_time = 5;
334
335        Self {
336            nodes,
337            format_scratch: String::with_capacity(1024),
338
339            sys: System {
340                unique_id: INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed),
341                z_cursor: 0.0,
342                theme: KERU_DARK,
343                inspect_mode: false,
344                debug_key_pressed: false,
345
346                new_ui_input: 2,
347                new_external_events: true,
348
349                clipboard: Clipboard::new().expect("Couldn't initialize clipboard"),
350
351                text: TextSystem {
352                    cache,
353                    atlas,
354                    text_renderer,
355                    font_system,
356                    slabs: TextSlabs {
357                        boxes: Vec::with_capacity(50),
358                        editors: Slab::with_capacity(10),
359                    },
360                    glyphon_viewport,
361                },
362
363                texture_atlas,
364
365                render_pipeline,
366                rects: Vec::with_capacity(100),
367                editor_rects_i: 0,
368                
369                click_rects: Vec::with_capacity(50),
370                scroll_rects: Vec::with_capacity(20),
371
372                gpu_rect_buffer,
373                base_uniform_buffer: resolution_buffer,
374                bind_group,
375
376                partial_relayout_count: 0,
377
378                current_frame: FIRST_FRAME,
379                third_last_frame_end_fake_time,
380                second_last_frame_end_fake_time,
381                last_frame_end_fake_time,
382
383                unifs: uniforms,
384
385                mouse_hit_stack: Vec::with_capacity(50),
386
387                mouse_input: MouseInput::default(),
388                key_input: KeyInput::default(),
389
390                // todo: maybe remove and use mouse_input.current_tag()? There was never a point in having multiple hovereds
391                hovered: Vec::with_capacity(15),
392                hovered_scroll_area: None,
393
394                #[cfg(debug_assertions)]
395                inspect_hovered: None,
396            
397                old_child_collect: Vec::with_capacity(10),
398                new_child_collect: Vec::with_capacity(10),
399                added_nodes: Vec::with_capacity(30),
400                direct_removed_nodes: Vec::with_capacity(30),
401                indirect_removed_nodes: Vec::with_capacity(30),
402                very_indirect_removed_nodes: Vec::with_capacity(30),
403
404                focused: None,
405
406                anim_render_timer: AnimationRenderTimer::default(),
407
408                changes: PartialChanges::new(),
409                hidden_stack: Vec::with_capacity(10),
410                hidden_nodes: Vec::with_capacity(10),
411            },
412        }
413    }
414
415    /// Returns a reference to a GPU buffer holding basic information.
416    /// 
417    /// At the cost of some coupling, this can be reused in other rendering jobs.
418    /// 
419    /// Example usage in shader:
420    /// ```wgpu
421    /// struct Uniforms {
422    ///     @location(1) screen_resolution: vec2f,
423    ///     @location(0) t: f32,
424    /// };
425    /// ```
426    pub fn base_uniform_buffer(&self) -> &Buffer {
427        return &self.sys.base_uniform_buffer;
428    }
429
430    /// Set inspect mode. When inspect mode is active, all nodes will be shown, including stacks and containers. 
431    pub fn set_inspect_mode(&mut self, inspect_mode: bool) {
432        if self.inspect_mode() != inspect_mode {
433            self.sys.changes.tree_changed = true;
434        }
435        self.sys.inspect_mode = inspect_mode;
436    }
437
438    /// Get the current inspect mode state.
439    /// When inspect mode is active, all nodes will be shown, including stacks and containers.
440    pub fn inspect_mode(&self) -> bool {
441        return self.sys.inspect_mode;
442    }
443
444    /// Get a reference to the active theme.
445    pub fn theme(&mut self) -> &mut Theme {
446        return &mut self.sys.theme;
447    }
448
449    pub fn current_frame(&self) -> u64 {
450        return self.sys.current_frame;
451    }
452
453    pub fn unique_id(&self) -> u64 {
454        return self.sys.unique_id;
455    }
456
457    pub fn push_external_event(&mut self) {
458        self.sys.new_external_events = true;
459    }
460
461    /// Returns `true` if the [`Ui`] needs to be updated.
462    /// 
463    /// This is true when the [`Ui`] received an input that it cares about, such as a click on a clickable element, or when the user explicitly notified it with [`Ui::push_external_event()`].
464    ///  
465    /// In a typical `winit` loop for an application that only updates in response to user input, this function is what decides if the [`Ui`] building code should be rerun.
466    /// 
467    /// In applications that update on every frame regardless of user input, like games or simulations, the [`Ui`] building code should be rerun on every frame unconditionally, so this function isn't useful.
468    pub fn needs_update(&mut self) -> bool {
469        return self.sys.new_ui_input > 0 ||
470            self.sys.new_external_events;
471    }
472
473    /// Returns `true` if the [`Ui`] needs to be updated or rerendered as a consequence of input, animations, or other [`Ui`]-internal events.
474    /// 
475    /// In a typical `winit` loop for an application that only updates in response to user input, this function is what decides if `winit::Window::request_redraw()` should be called.
476    /// 
477    /// For an application that updates on every frame regardless of user input, like a game or a simulation, `request_redraw()` should be called on every frame unconditionally, so this function isn't useful.
478    pub fn event_loop_needs_to_wake(&mut self) -> bool {
479        return self.needs_update() || self.needs_rerender();
480    }
481
482    pub fn cursor_position(&self) -> DVec2 {
483        return self.sys.mouse_input.cursor_position();
484    }
485
486    // todo: expose functions directly instead of the inner struct
487    pub fn key_input(&self) -> &KeyInput {
488        return &self.sys.key_input;
489    }
490
491    pub(crate) fn set_new_ui_input(&mut self) {
492        // Anti state-tearing: always update two times
493        // Or rather, anti get-stuck-in-a-state-teared-frame. The state tearing is still there for one frame.
494        self.sys.new_ui_input = 2;
495    }
496
497    /// Resize the `Ui`. 
498    /// Updates the `Ui`'s internal state, and schedules a full relayout to adapt to the new size.
499    /// Called by [`Ui::window_event`].
500    pub(crate) fn resize(&mut self, size: &PhysicalSize<u32>) {        
501        self.sys.changes.full_relayout = true;
502        
503        self.sys.unifs.size[X] = size.width as f32;
504        self.sys.unifs.size[Y] = size.height as f32;
505
506        self.sys.changes.resize = true;
507        self.set_new_ui_input();
508    }
509}
510
511impl Ui {
512    pub(crate) fn hit_click_rect(&self, rect: &ClickRect) -> bool {
513        let size = self.sys.unifs.size;
514        let cursor_pos = (
515            self.cursor_position().x as f32 / size[X],
516            self.cursor_position().y as f32 / size[Y],
517        );
518
519        let aabb_hit = rect.rect[X][0] < cursor_pos.0
520            && cursor_pos.0 < rect.rect[X][1]
521            && rect.rect[Y][0] < cursor_pos.1
522            && cursor_pos.1 < rect.rect[Y][1];
523
524        if aabb_hit == false {
525            return false;
526        }
527
528        let node_i = rect.i;
529
530
531        match self.nodes[node_i].params.rect.shape {
532            Shape::Rectangle { corner_radius: _ } => {
533                return true;
534            }
535            Shape::Circle => {
536                // Calculate the circle center and radius
537                let center_x = (rect.rect[X][0] + rect.rect[X][1]) / 2.0;
538                let center_y = (rect.rect[Y][0] + rect.rect[Y][1]) / 2.0;
539                let radius = (rect.rect[X][1] - rect.rect[X][0]) / 2.0;
540
541                // Check if the mouse is within the circle
542                let dx = cursor_pos.0 - center_x;
543                let dy = cursor_pos.1 - center_y;
544                return dx * dx + dy * dy <= radius * radius;
545            }
546            Shape::Ring { width } => {
547                // scale to correct coordinates
548                // width should have been a Len anyway so this will have to change
549                let width = width / size[X];
550
551                let aspect = size[X] / size[Y];
552                    // Calculate the ring's center and radii
553                let center_x = (rect.rect[X][0] + rect.rect[X][1]) / 2.0;
554                let center_y = (rect.rect[Y][0] + rect.rect[Y][1]) / 2.0;
555                let outer_radius = (rect.rect[X][1] - rect.rect[X][0]) / 2.0;
556                let inner_radius = outer_radius - width;
557
558                // Check if the mouse is within the ring
559                let dx = cursor_pos.0 - center_x;
560                let dy = (cursor_pos.1 - center_y) / aspect;
561                let distance_squared = dx * dx + dy * dy;
562                return distance_squared <= outer_radius * outer_radius
563                    && distance_squared >= inner_radius * inner_radius;
564
565            }
566        }
567
568    }
569
570    pub(crate) fn set_static_image(&mut self, i: NodeI, image: &'static [u8]) -> &mut Self {
571        let image_pointer: *const u8 = image.as_ptr();
572
573        if let Some(last_pointer) = self.nodes[i].last_static_image_ptr {
574            if image_pointer == last_pointer {
575                return self;
576            }
577        }
578
579        let image = self.sys.texture_atlas.allocate_image(image);
580        self.nodes[i].imageref = Some(image);
581        self.nodes[i].last_static_image_ptr = Some(image_pointer);
582
583        return self;
584    }
585}