pix_engine/gui/
state.rs

1//! GUI State.
2
3use super::theme::FontId;
4use crate::{
5    gui::{keys::KeyState, mouse::MouseState},
6    prelude::*,
7};
8use lru::LruCache;
9use std::{
10    collections::{hash_map::DefaultHasher, HashSet},
11    convert::TryInto,
12    error::Error,
13    fmt,
14    hash::{Hash, Hasher},
15    mem,
16    ops::{Deref, DerefMut},
17    str::FromStr,
18};
19
20/// A hashed element identifier for internal state management.
21#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
22pub struct ElementId(pub u64);
23
24impl ElementId {
25    const NONE: Self = ElementId(0);
26}
27
28impl fmt::Display for ElementId {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "{}", self.0)
31    }
32}
33
34impl Deref for ElementId {
35    type Target = u64;
36    fn deref(&self) -> &Self::Target {
37        &self.0
38    }
39}
40
41impl DerefMut for ElementId {
42    fn deref_mut(&mut self) -> &mut Self::Target {
43        &mut self.0
44    }
45}
46
47impl From<ElementId> for u64 {
48    fn from(id: ElementId) -> Self {
49        *id
50    }
51}
52
53const ELEMENT_CACHE_SIZE: usize = 128;
54
55/// UI Texture with source and destination.
56#[derive(Default, Debug, Clone, Eq, PartialEq, Hash)]
57pub(crate) struct Texture {
58    pub(crate) id: TextureId,
59    pub(crate) element_id: ElementId,
60    pub(crate) src: Option<Rect<i32>>,
61    pub(crate) dst: Option<Rect<i32>>,
62    pub(crate) visible: bool,
63    pub(crate) font_id: FontId,
64    pub(crate) font_size: u32,
65}
66
67impl Texture {
68    pub(crate) const fn new(
69        id: TextureId,
70        element_id: ElementId,
71        src: Option<Rect<i32>>,
72        dst: Option<Rect<i32>>,
73        font_id: FontId,
74        font_size: u32,
75    ) -> Self {
76        Self {
77            id,
78            element_id,
79            src,
80            dst,
81            visible: true,
82            font_id,
83            font_size,
84        }
85    }
86}
87
88/// Internal tracked UI state.
89#[derive(Debug)]
90pub(crate) struct UiState {
91    /// Current global render position, in window coordinates.
92    cursor: Point<i32>,
93    /// Previous global render position, in window coordinates.
94    pcursor: Point<i32>,
95    /// Offset for global render position, in window coordinates.
96    column_offset: i32,
97    /// Current line height.
98    pub(crate) line_height: i32,
99    /// Previous line height.
100    pub(crate) pline_height: i32,
101    /// Temporary stack of cursor positions.
102    cursor_stack: Vec<(Point<i32>, Point<i32>, i32, i32)>,
103    /// Temporary stack of cursor offset.
104    offset_stack: Vec<i32>,
105    /// ID stack to assist with generating unique element IDs.
106    id_stack: Vec<u64>,
107    /// Override for max-width elements.
108    pub(crate) next_width: Option<i32>,
109    /// UI texture to be drawn over rendered frame, in rendered order.
110    pub(crate) textures: Vec<Texture>,
111    /// Whether UI elements are disabled.
112    pub(crate) disabled: bool,
113    /// Mouse state for the current frame.
114    pub(crate) mouse: MouseState,
115    /// Mouse position offset for rendering within textures and viewports.
116    pub(crate) mouse_offset: Option<Point<i32>>,
117    /// Mouse state for the previous frame.
118    pub(crate) pmouse: MouseState,
119    /// Keyboard state for the current frame.
120    pub(crate) keys: KeyState,
121    /// Element state for the current frame,
122    pub(crate) elements: LruCache<ElementId, ElementState>,
123    /// Which element is active.
124    active: Option<ElementId>,
125    /// Which element is hovered.
126    hovered: Option<ElementId>,
127    /// Which element is focused.
128    focused: Option<ElementId>,
129    /// Which element is being edited.
130    editing: Option<ElementId>,
131    /// Whether elements can be focused or not.
132    focus_enabled: bool,
133    /// Last focusable element rendered.
134    last_focusable: Option<ElementId>,
135    /// Last bounding box rendered.
136    last_size: Option<Rect<i32>>,
137}
138
139impl Default for UiState {
140    #[allow(clippy::expect_used)]
141    fn default() -> Self {
142        Self {
143            cursor: point![],
144            pcursor: point![],
145            column_offset: 0,
146            line_height: 0,
147            pline_height: 0,
148            cursor_stack: vec![],
149            offset_stack: vec![],
150            id_stack: vec![],
151            next_width: None,
152            textures: vec![],
153            disabled: false,
154            mouse: MouseState::default(),
155            mouse_offset: None,
156            pmouse: MouseState::default(),
157            keys: KeyState::default(),
158            elements: LruCache::new(ELEMENT_CACHE_SIZE.try_into().expect("valid cache size")),
159            active: None,
160            hovered: None,
161            focused: Some(ElementId::NONE),
162            editing: None,
163            focus_enabled: true,
164            last_focusable: None,
165            last_size: None,
166        }
167    }
168}
169
170impl UiState {
171    /// Handle state changes this frame prior to calling [`PixEngine::on_update`].
172    #[inline]
173    pub(crate) fn pre_update(&mut self, theme: &Theme) {
174        self.clear_hovered();
175
176        self.pcursor = point![];
177        self.cursor = theme.spacing.frame_pad;
178        self.column_offset = 0;
179    }
180
181    /// Handle state changes this frame after calling [`PixEngine::on_update`].
182    #[inline]
183    pub(crate) fn post_update(&mut self) {
184        for texture in &mut self.textures {
185            texture.visible = false;
186        }
187
188        self.pmouse.pos = self.mouse.pos;
189        if !self.mouse.is_down(Mouse::Left) {
190            self.clear_active();
191        } else if !self.has_active() {
192            // Disable focused state while mouse is down from previous frame
193            self.set_active(ElementId(0));
194        }
195        self.clear_entered();
196    }
197
198    /// Helper function to hash element labels.
199    #[inline]
200    #[must_use]
201    pub(crate) fn get_id<T: Hash>(&self, t: &T) -> ElementId {
202        let mut hasher = DefaultHasher::new();
203        t.hash(&mut hasher);
204        if let Some(id) = self.id_stack.last() {
205            id.hash(&mut hasher);
206        }
207        ElementId(hasher.finish())
208    }
209
210    /// Helper to strip out any ID-specific patterns from a label.
211    #[inline]
212    #[must_use]
213    // FIXME: In the future labels will require internal state.
214    #[allow(clippy::unused_self)]
215    pub(crate) fn get_label<'a>(&self, label: &'a str) -> &'a str {
216        label.split("##").next().unwrap_or("")
217    }
218
219    /// Returns the current UI rendering position.
220    #[inline]
221    pub(crate) const fn cursor(&self) -> Point<i32> {
222        self.cursor
223    }
224
225    /// Set the current UI rendering position.
226    #[inline]
227    pub(crate) fn set_cursor<P: Into<Point<i32>>>(&mut self, cursor: P) {
228        self.cursor = cursor.into();
229    }
230
231    /// Returns the previous UI rendering position.
232    #[inline]
233    pub(crate) const fn pcursor(&self) -> Point<i32> {
234        self.pcursor
235    }
236
237    /// Returns the current offset for the current UI rendering position.
238    #[inline]
239    pub(crate) const fn column_offset(&self) -> i32 {
240        self.column_offset
241    }
242
243    /// Set an offset for the current UI rendering position.
244    #[inline]
245    pub(crate) fn set_column_offset(&mut self, offset: i32) {
246        self.offset_stack.push(offset);
247        self.cursor.offset_x(offset);
248        self.column_offset += offset;
249    }
250
251    /// Restore any offsets for the current UI rendering position.
252    #[inline]
253    pub(crate) fn reset_column_offset(&mut self) {
254        let offset = self.offset_stack.pop().unwrap_or_default();
255        self.cursor.offset_x(-offset);
256        self.column_offset -= offset;
257    }
258
259    /// Push a new UI rendering position to the stack.
260    #[inline]
261    pub(crate) fn push_cursor(&mut self) {
262        self.cursor_stack.push((
263            self.pcursor,
264            self.cursor,
265            self.pline_height,
266            self.line_height,
267        ));
268    }
269
270    /// Pop a new UI rendering position from the stack.
271    #[inline]
272    pub(crate) fn pop_cursor(&mut self) {
273        let (pcursor, cursor, pline_height, line_height) =
274            self.cursor_stack.pop().unwrap_or_default();
275        self.pcursor = pcursor;
276        self.cursor = cursor;
277        self.pline_height = pline_height;
278        self.line_height = line_height;
279    }
280
281    /// Returns the current mouse position coordinates as `(x, y)`.
282    #[inline]
283    pub(crate) fn mouse_pos(&self) -> Point<i32> {
284        let mut pos = self.mouse.pos;
285        if let Some(offset) = self.mouse_offset {
286            pos.offset(-offset);
287        }
288        pos
289    }
290
291    /// Returns the previous mouse position coordinates last frame as `(x, y)`.
292    #[inline]
293    pub(crate) fn pmouse_pos(&self) -> Point<i32> {
294        let mut pos = self.pmouse.pos;
295        if let Some(offset) = self.mouse_offset {
296            pos.offset(-offset);
297        }
298        pos
299    }
300
301    /// Returns if any [Mouse] button was pressed this frame.
302    #[inline]
303    #[must_use]
304    pub(crate) fn mouse_pressed(&self) -> bool {
305        self.mouse.is_pressed()
306    }
307
308    /// Returns if the [Mouse] was clicked (pressed and released) this frame.
309    #[inline]
310    #[must_use]
311    pub(crate) fn mouse_clicked(&self, btn: Mouse) -> bool {
312        self.mouse.was_clicked(btn)
313    }
314
315    /// Returns if the [Mouse] was double clicked (pressed and released) this frame.
316    #[inline]
317    #[must_use]
318    pub(crate) fn mouse_dbl_clicked(&self, btn: Mouse) -> bool {
319        self.mouse.was_dbl_clicked(btn)
320    }
321
322    /// Returns if a specific [Mouse] button was pressed this frame.
323    #[inline]
324    #[must_use]
325    pub(crate) fn mouse_down(&self, btn: Mouse) -> bool {
326        self.mouse.is_down(btn)
327    }
328
329    /// Returns a list of the current mouse buttons pressed this frame.
330    #[inline]
331    #[must_use]
332    pub(crate) const fn mouse_buttons(&self) -> &HashSet<Mouse> {
333        self.mouse.pressed()
334    }
335
336    /// Returns if any [Key] was pressed this frame.
337    #[inline]
338    #[must_use]
339    pub(crate) fn key_pressed(&self) -> bool {
340        self.keys.is_pressed()
341    }
342
343    /// Returns if a specific [Key] was pressed this frame.
344    #[inline]
345    #[must_use]
346    pub(crate) fn key_down(&self, key: Key) -> bool {
347        self.keys.is_down(key)
348    }
349
350    /// Returns a list of the current keys pressed this frame.
351    #[inline]
352    #[must_use]
353    pub(crate) const fn keys(&self) -> &HashSet<Key> {
354        self.keys.pressed()
355    }
356
357    /// Returns if a specific [`KeyMod`] was pressed this frame.
358    #[inline]
359    #[must_use]
360    pub(crate) const fn keymod_down(&self, keymod: KeyMod) -> bool {
361        self.keys.mod_down(keymod)
362    }
363
364    /// Returns a list of the current key modifiers pressed this frame.
365    #[inline]
366    pub(crate) const fn keymod(&self) -> &KeyMod {
367        self.keys.keymod()
368    }
369
370    /// Set a mouse offset for rendering within textures or viewports.
371    #[inline]
372    pub(crate) fn offset_mouse<P: Into<Point<i32>>>(&mut self, offset: P) {
373        self.mouse_offset = Some(offset.into());
374    }
375
376    /// Clear mouse offset for rendering within textures or viewports.
377    #[inline]
378    pub(crate) fn clear_mouse_offset(&mut self) {
379        self.mouse_offset = None;
380    }
381
382    /// Whether an element is `active` or not. An element is marked `active` when there is no other
383    /// `active` elements, it is marked `hovered` and receives a mouse down event for the
384    /// [`Mouse::Left`] button. `active` is cleared after every frame.
385    #[inline]
386    #[must_use]
387    pub(crate) fn is_active(&self, id: ElementId) -> bool {
388        !self.disabled && matches!(self.active, Some(el) if el == id)
389    }
390
391    /// Whether any element is currently `active`.
392    #[inline]
393    #[must_use]
394    pub(crate) const fn has_active(&self) -> bool {
395        self.active.is_some()
396    }
397
398    /// Set a given element as `active`.
399    #[inline]
400    pub(crate) fn set_active(&mut self, id: ElementId) {
401        self.active = Some(id);
402    }
403
404    /// Clears the current `active` element.
405    #[inline]
406    pub(crate) fn clear_active(&mut self) {
407        self.active = None;
408    }
409
410    /// Whether an element is `hovered` or not. When an element is considered `hovered` depends on
411    /// the widget, but generally involves checking if the [`PixState::mouse_pos`] is within the
412    /// elements bounding area.
413    #[inline]
414    #[must_use]
415    pub(crate) fn is_hovered(&self, id: ElementId) -> bool {
416        matches!(self.hovered, Some(el) if el == id)
417    }
418
419    /// Whether any element currently is `hovered`.
420    #[inline]
421    #[must_use]
422    pub(crate) const fn has_hover(&self) -> bool {
423        self.hovered.is_some()
424    }
425
426    /// Set a given element as `hovered` and check for [`Mouse::Left`] being down to set `active`
427    /// only if there are no other `active` elements. `active` is cleared after every frame.
428    #[inline]
429    pub(crate) fn hover(&mut self, id: ElementId) {
430        self.hovered = Some(id);
431        // If mouse is down this frame while hovered, make element active, if something else isn't
432        // already
433        if !self.has_active() && self.mouse.is_down(Mouse::Left) {
434            self.set_active(id);
435        }
436    }
437
438    /// Clears the current `hovered` element.
439    #[inline]
440    pub(crate) fn clear_hovered(&mut self) {
441        self.hovered = None;
442    }
443
444    /// Try to capture `hover` if no other element is currently `hovered`.
445    #[inline]
446    pub(crate) fn try_hover<S: Contains<Point<i32>>>(&mut self, id: ElementId, shape: &S) -> bool {
447        if !self.has_hover() && !self.disabled && shape.contains(self.mouse_pos()) {
448            self.hover(id);
449        }
450        self.is_hovered(id)
451    }
452
453    /// Whether an element is `focused` or not. An element is `focused` when it captures it via
454    /// tab-cycling, or if it is clicked.
455    #[inline]
456    #[must_use]
457    pub(crate) fn is_focused(&self, id: ElementId) -> bool {
458        !self.disabled && matches!(self.focused, Some(el) if el == id)
459    }
460
461    /// Whether any element currently has `focus`.
462    #[inline]
463    #[must_use]
464    pub(crate) const fn has_focused(&self) -> bool {
465        self.focused.is_some()
466    }
467
468    /// Set a given element as `focused`.
469    #[inline]
470    pub(crate) fn focus(&mut self, id: ElementId) {
471        self.focused = Some(id);
472    }
473
474    /// Try to capture `focus` if no other element is currently `focused`. This supports tab-cycling
475    /// through elements with the keyboard.
476    #[inline]
477    pub(crate) fn try_focus(&mut self, id: ElementId) -> bool {
478        if !self.disabled && !self.has_focused() {
479            self.focus(id);
480        }
481        self.is_focused(id)
482    }
483
484    /// Clears the current `focused` element.
485    #[inline]
486    pub(crate) fn blur(&mut self) {
487        self.focused = Some(ElementId::NONE);
488    }
489
490    /// Whether an element is being edited or not.
491    #[inline]
492    #[must_use]
493    pub(crate) fn is_editing(&self, id: ElementId) -> bool {
494        !self.disabled && matches!(self.editing, Some(el) if el == id)
495    }
496
497    /// Start edit mode for a given element.
498    #[inline]
499    pub(crate) fn begin_edit(&mut self, id: ElementId) {
500        self.editing = Some(id);
501    }
502
503    /// End edit mode for a given element.
504    #[inline]
505    pub(crate) fn end_edit(&mut self) {
506        self.editing = None;
507    }
508
509    /// Disable global focus interaction.
510    #[inline]
511    pub(crate) fn disable_focus(&mut self) {
512        self.focus_enabled = false;
513    }
514
515    /// Enable global focus interaction.
516    #[inline]
517    pub(crate) fn enable_focus(&mut self) {
518        self.focus_enabled = true;
519    }
520
521    /// Handles global element inputs for `focused` checks.
522    #[inline]
523    pub(crate) fn handle_focus(&mut self, id: ElementId) {
524        if !self.focus_enabled {
525            return;
526        }
527        let active = self.is_active(id);
528        let hovered = self.is_hovered(id);
529        let focused = self.is_focused(id);
530        if self.keys.was_entered(Key::Tab) {
531            // Tab-focus cycling
532            // If element is focused when Tab pressed, clear it so the next element can capture focus.
533            // If SHIFT was held, re-focus the last element rendered
534            // Clear keys, so next element doesn't trigger tab logic
535            let none_focused = self.focused == Some(ElementId::NONE);
536            if none_focused || focused {
537                if self.keys.mod_down(KeyMod::SHIFT) {
538                    self.focused = self.last_focusable;
539                    self.clear_entered();
540                } else if focused {
541                    self.focused = None;
542                    self.clear_entered();
543                } else if none_focused {
544                    self.focused = Some(id);
545                    self.clear_entered();
546                }
547            }
548        } else if !self.mouse.is_down(Mouse::Left) && active && hovered {
549            // Click focusing on release
550            self.focus(id);
551        } else if focused && self.mouse.is_down(Mouse::Left) && !active && !hovered {
552            // Blur on outside click
553            self.blur();
554        }
555        self.last_focusable = Some(id);
556    }
557
558    /// Whether this element was `clicked` this frame. Treats [`Key::Return`] being pressed while
559    /// focused a a `click`.
560    ///
561    /// If element is hovered and active, it means it was clicked last frame
562    /// If mouse isn't down this frame, it means mouse was both clicked and released above an
563    /// element
564    #[inline]
565    #[must_use]
566    pub(crate) fn was_clicked(&mut self, id: ElementId) -> bool {
567        // Enter simulates a click
568        if self.is_focused(id) && self.keys.was_entered(Key::Return) {
569            self.clear_entered();
570            true
571        } else {
572            // Mouse is up, but we're hovered and active so user must have clicked
573            !self.mouse.is_down(Mouse::Left) && self.is_hovered(id) && self.is_active(id)
574        }
575    }
576
577    /// Return what, if any, [Key] was entered this frame. This is cleared at the end of each
578    /// frame.
579    #[inline]
580    #[must_use]
581    pub(crate) const fn key_entered(&self) -> Option<Key> {
582        self.keys.entered
583    }
584
585    /// Clear all per-frame events.
586    #[inline]
587    pub(crate) fn clear_entered(&mut self) {
588        self.keys.typed = None;
589        self.keys.entered = None;
590        self.mouse.clicked.clear();
591        self.mouse.xrel = 0;
592        self.mouse.yrel = 0;
593    }
594
595    /// Returns the current `scroll` state for this element.
596    #[inline]
597    pub(crate) fn scroll(&self, id: ElementId) -> Vector<i32> {
598        self.elements
599            .peek(&id)
600            .map_or_else(Vector::default, |state| state.scroll)
601    }
602
603    /// Set the current `scroll` state for this element.
604    #[inline]
605    pub(crate) fn set_scroll(&mut self, id: ElementId, scroll: Vector<i32>) {
606        if let Some(state) = self.elements.get_mut(&id) {
607            state.scroll = scroll;
608        } else {
609            self.elements.put(
610                id,
611                ElementState {
612                    scroll,
613                    ..ElementState::default()
614                },
615            );
616        }
617    }
618
619    /// Returns the current `text_edit` state for this element.
620    #[inline]
621    #[must_use]
622    pub(crate) fn text_edit<S>(&mut self, id: ElementId, initial_text: S) -> String
623    where
624        S: Into<String>,
625    {
626        self.elements.get_mut(&id).map_or_else(
627            || initial_text.into(),
628            |state| mem::take(&mut state.text_edit),
629        )
630    }
631
632    /// Updates the current `text_edit` state for this element.
633    #[inline]
634    pub(crate) fn set_text_edit(&mut self, id: ElementId, text_edit: String) {
635        if let Some(state) = self.elements.get_mut(&id) {
636            state.text_edit = text_edit;
637        } else {
638            self.elements.put(
639                id,
640                ElementState {
641                    text_edit,
642                    ..ElementState::default()
643                },
644            );
645        }
646    }
647
648    /// Parses the current `text_edit` state for this element into a given type.
649    #[inline]
650    #[must_use]
651    pub(crate) fn parse_text_edit<T>(&mut self, id: ElementId, default: T) -> T
652    where
653        T: FromStr + Copy,
654        <T as FromStr>::Err: Error + Sync + Send + 'static,
655    {
656        self.elements
657            .pop(&id)
658            .map_or(default, |state| state.text_edit.parse().unwrap_or(default))
659    }
660
661    /// Returns whether the current element is expanded or not.
662    #[inline]
663    #[must_use]
664    pub(crate) fn expanded(&mut self, id: ElementId) -> bool {
665        self.elements
666            .get_mut(&id)
667            .map_or(false, |state| state.expanded)
668    }
669
670    /// Set whether the current element is expanded or not.
671    #[inline]
672    pub(crate) fn set_expanded(&mut self, id: ElementId, expanded: bool) {
673        if let Some(state) = self.elements.get_mut(&id) {
674            state.expanded = expanded;
675        } else {
676            self.elements.put(
677                id,
678                ElementState {
679                    expanded,
680                    ..ElementState::default()
681                },
682            );
683        }
684    }
685
686    /// Returns the width of the last rendered UI element, or 0 if there is no last rendered
687    /// element.
688    #[inline]
689    #[must_use]
690    pub(crate) fn last_width(&self) -> i32 {
691        self.last_size.map(|s| s.width()).unwrap_or_default()
692    }
693}
694
695impl PixState {
696    /// Push a new seed to the UI ID stack. Helps in generating unique widget identifiers that have
697    /// the same text label. Pushing a unique ID to the stack will seed the hash of the label.
698    #[inline]
699    pub fn push_id<I>(&mut self, id: I)
700    where
701        I: TryInto<u64>,
702    {
703        self.ui.id_stack.push(id.try_into().unwrap_or(1));
704    }
705
706    /// Pop a seed from the UI ID stack.
707    #[inline]
708    pub fn pop_id(&mut self) {
709        self.ui.id_stack.pop();
710    }
711
712    /// Returns the current UI rendering position.
713    ///
714    /// # Example
715    ///
716    /// ```
717    /// # use pix_engine::prelude::*;
718    /// # struct App;
719    /// # impl PixEngine for App {
720    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
721    ///     let mut pos = s.cursor_pos();
722    ///     pos.offset_y(20);
723    ///     s.set_cursor_pos(pos);
724    ///     s.text("Some text, offset down by 20 pixels")?;
725    ///     Ok(())
726    /// }
727    /// # }
728    /// ```
729    #[inline]
730    pub const fn cursor_pos(&self) -> Point<i32> {
731        self.ui.cursor()
732    }
733
734    /// Set the current UI rendering position.
735    ///
736    /// # Example
737    ///
738    /// ```
739    /// # use pix_engine::prelude::*;
740    /// # struct App;
741    /// # impl PixEngine for App {
742    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
743    ///     s.set_cursor_pos(s.center()?);
744    ///     s.rect_mode(RectMode::Center);
745    ///     s.text("Centered text")?;
746    ///     Ok(())
747    /// }
748    /// # }
749    /// ```
750    #[inline]
751    pub fn set_cursor_pos<P: Into<Point<i32>>>(&mut self, cursor: P) {
752        self.ui.set_cursor(cursor.into());
753    }
754
755    /// Set the current UI rendering position column offset.
756    ///
757    #[inline]
758    pub fn set_column_offset(&mut self, offset: i32) {
759        self.ui.set_column_offset(offset);
760    }
761
762    /// Clears the current UI rendering position column offset.
763    ///
764    #[inline]
765    pub fn reset_column_offset(&mut self) {
766        self.ui.reset_column_offset();
767    }
768
769    /// Returns whether the last item drawn is hovered with the mouse.
770    ///
771    /// # Example
772    ///
773    /// ```
774    /// # use pix_engine::prelude::*;
775    /// # struct App;
776    /// # impl PixEngine for App {
777    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
778    ///     s.text("Hover me")?;
779    ///     if s.hovered() {
780    ///         s.tooltip("I'm a tooltip!");
781    ///     }
782    ///     Ok(())
783    /// }
784    /// # }
785    /// ```
786    #[inline]
787    #[must_use]
788    pub fn hovered(&self) -> bool {
789        self.ui.last_size.map_or(false, |rect| {
790            !self.ui.disabled && rect.contains(self.mouse_pos())
791        })
792    }
793
794    /// Returns whether the last item drawn was clicked with the left mouse button.
795    ///
796    /// # Example
797    ///
798    /// ```
799    /// # use pix_engine::prelude::*;
800    /// # struct App;
801    /// # impl PixEngine for App {
802    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
803    ///     s.text("Hover me")?;
804    ///     if s.clicked() {
805    ///         println!("I was clicked!");
806    ///     }
807    ///     Ok(())
808    /// }
809    /// # }
810    /// ```
811    #[inline]
812    #[must_use]
813    pub fn clicked(&self) -> bool {
814        self.ui.last_size.map_or(false, |rect| {
815            !self.ui.disabled && self.mouse_clicked(Mouse::Left) && rect.contains(self.mouse_pos())
816        })
817    }
818
819    /// Returns whether the last item drawn was double-clicked with the left mouse button.
820    ///
821    /// # Example
822    ///
823    /// ```
824    /// # use pix_engine::prelude::*;
825    /// # struct App;
826    /// # impl PixEngine for App {
827    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
828    ///     s.text("Hover me")?;
829    ///     if s.dbl_clicked() {
830    ///         println!("I was double clicked!");
831    ///     }
832    ///     Ok(())
833    /// }
834    /// # }
835    /// ```
836    #[inline]
837    #[must_use]
838    pub fn dbl_clicked(&self) -> bool {
839        self.ui.last_size.map_or(false, |rect| {
840            !self.ui.disabled
841                && self.mouse_clicked(Mouse::Left)
842                && self.mouse_dbl_clicked(Mouse::Left)
843                && rect.contains(self.mouse_pos())
844        })
845    }
846}
847
848impl PixState {
849    /// Advance the current UI cursor position for an element.
850    #[inline]
851    pub(crate) fn advance_cursor<S: Into<Point<i32>>>(&mut self, size: S) {
852        let size = size.into();
853        let pos = self.ui.cursor;
854        let padx = self.theme.spacing.frame_pad.x();
855        let pady = self.theme.spacing.item_pad.y();
856        let offset_x = self.ui.column_offset;
857
858        // Previous cursor ends at the right of this item
859        self.ui.pcursor = point![pos.x() + size.x(), pos.y()];
860        if self.settings.rect_mode == RectMode::Center {
861            self.ui.pcursor.offset(-size / 2);
862        }
863
864        if cfg!(feature = "debug_ui") {
865            self.push();
866            self.fill(None);
867            self.stroke(Color::RED);
868            let _result = self.rect(rect![pos, size.x(), size.y()]);
869            self.fill(Color::BLUE);
870            let _result = self.circle(circle![self.ui.pcursor(), 3]);
871            self.pop();
872        }
873
874        // Move cursor to the next line with padding, choosing the maximum of the next line or the
875        // previous y value to account for variable line heights when using `same_line`.
876        let line_height = self.ui.line_height.max(size.y());
877        self.ui.cursor = point![padx + offset_x, pos.y() + line_height + pady];
878        self.ui.pline_height = line_height;
879        self.ui.line_height = 0;
880        self.ui.last_size = Some(rect![pos, size.x(), size.y()]);
881    }
882
883    /// Get or create a UI texture to render to
884    #[inline]
885    pub(crate) fn get_or_create_texture<R>(
886        &mut self,
887        id: ElementId,
888        src: R,
889        dst: Rect<i32>,
890    ) -> PixResult<TextureId>
891    where
892        R: Into<Option<Rect<i32>>>,
893    {
894        let font_id = self.theme.fonts.body.id();
895        let font_size = self.theme.font_size;
896        if let Some(texture) = self
897            .ui
898            .textures
899            .iter_mut()
900            .find(|t| t.element_id == id && t.font_id == font_id && t.font_size == font_size)
901        {
902            texture.visible = true;
903            texture.dst = Some(dst);
904            Ok(texture.id)
905        } else {
906            let texture_id =
907                self.create_texture(dst.width() as u32, dst.height() as u32, PixelFormat::Rgba)?;
908            self.ui.textures.push(Texture::new(
909                texture_id,
910                id,
911                src.into(),
912                Some(dst),
913                font_id,
914                font_size,
915            ));
916            Ok(texture_id)
917        }
918    }
919}
920
921/// Internal tracked UI element state.
922#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
923pub(crate) struct ElementState {
924    scroll: Vector<i32>,
925    text_edit: String,
926    current_tab: usize,
927    expanded: bool,
928}