use super::theme::FontId;
use crate::{
gui::{keys::KeyState, mouse::MouseState},
prelude::*,
};
use lru::LruCache;
use std::{
collections::{hash_map::DefaultHasher, HashSet},
convert::TryInto,
error::Error,
fmt,
hash::{Hash, Hasher},
mem,
ops::{Deref, DerefMut},
str::FromStr,
};
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ElementId(pub u64);
impl ElementId {
const NONE: Self = ElementId(0);
}
impl fmt::Display for ElementId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for ElementId {
type Target = u64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ElementId {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<ElementId> for u64 {
fn from(id: ElementId) -> Self {
*id
}
}
const ELEMENT_CACHE_SIZE: usize = 128;
#[derive(Default, Debug, Clone, Eq, PartialEq, Hash)]
pub(crate) struct Texture {
pub(crate) id: TextureId,
pub(crate) element_id: ElementId,
pub(crate) src: Option<Rect<i32>>,
pub(crate) dst: Option<Rect<i32>>,
pub(crate) visible: bool,
pub(crate) font_id: FontId,
pub(crate) font_size: u32,
}
impl Texture {
pub(crate) const fn new(
id: TextureId,
element_id: ElementId,
src: Option<Rect<i32>>,
dst: Option<Rect<i32>>,
font_id: FontId,
font_size: u32,
) -> Self {
Self {
id,
element_id,
src,
dst,
visible: true,
font_id,
font_size,
}
}
}
#[derive(Debug)]
pub(crate) struct UiState {
cursor: Point<i32>,
pcursor: Point<i32>,
column_offset: i32,
pub(crate) line_height: i32,
pub(crate) pline_height: i32,
cursor_stack: Vec<(Point<i32>, Point<i32>, i32, i32)>,
offset_stack: Vec<i32>,
id_stack: Vec<u64>,
pub(crate) next_width: Option<i32>,
pub(crate) textures: Vec<Texture>,
pub(crate) disabled: bool,
pub(crate) mouse: MouseState,
pub(crate) mouse_offset: Option<Point<i32>>,
pub(crate) pmouse: MouseState,
pub(crate) keys: KeyState,
pub(crate) elements: LruCache<ElementId, ElementState>,
active: Option<ElementId>,
hovered: Option<ElementId>,
focused: Option<ElementId>,
editing: Option<ElementId>,
focus_enabled: bool,
last_focusable: Option<ElementId>,
last_size: Option<Rect<i32>>,
}
impl Default for UiState {
#[allow(clippy::expect_used)]
fn default() -> Self {
Self {
cursor: point![],
pcursor: point![],
column_offset: 0,
line_height: 0,
pline_height: 0,
cursor_stack: vec![],
offset_stack: vec![],
id_stack: vec![],
next_width: None,
textures: vec![],
disabled: false,
mouse: MouseState::default(),
mouse_offset: None,
pmouse: MouseState::default(),
keys: KeyState::default(),
elements: LruCache::new(ELEMENT_CACHE_SIZE.try_into().expect("valid cache size")),
active: None,
hovered: None,
focused: Some(ElementId::NONE),
editing: None,
focus_enabled: true,
last_focusable: None,
last_size: None,
}
}
}
impl UiState {
#[inline]
pub(crate) fn pre_update(&mut self, theme: &Theme) {
self.clear_hovered();
self.pcursor = point![];
self.cursor = theme.spacing.frame_pad;
self.column_offset = 0;
}
#[inline]
pub(crate) fn post_update(&mut self) {
for texture in &mut self.textures {
texture.visible = false;
}
self.pmouse.pos = self.mouse.pos;
if !self.mouse.is_down(Mouse::Left) {
self.clear_active();
} else if !self.has_active() {
self.set_active(ElementId(0));
}
self.clear_entered();
}
#[inline]
#[must_use]
pub(crate) fn get_id<T: Hash>(&self, t: &T) -> ElementId {
let mut hasher = DefaultHasher::new();
t.hash(&mut hasher);
if let Some(id) = self.id_stack.last() {
id.hash(&mut hasher);
}
ElementId(hasher.finish())
}
#[inline]
#[must_use]
#[allow(clippy::unused_self)]
pub(crate) fn get_label<'a>(&self, label: &'a str) -> &'a str {
label.split("##").next().unwrap_or("")
}
#[inline]
pub(crate) const fn cursor(&self) -> Point<i32> {
self.cursor
}
#[inline]
pub(crate) fn set_cursor<P: Into<Point<i32>>>(&mut self, cursor: P) {
self.cursor = cursor.into();
}
#[inline]
pub(crate) const fn pcursor(&self) -> Point<i32> {
self.pcursor
}
#[inline]
pub(crate) const fn column_offset(&self) -> i32 {
self.column_offset
}
#[inline]
pub(crate) fn set_column_offset(&mut self, offset: i32) {
self.offset_stack.push(offset);
self.cursor.offset_x(offset);
self.column_offset += offset;
}
#[inline]
pub(crate) fn reset_column_offset(&mut self) {
let offset = self.offset_stack.pop().unwrap_or_default();
self.cursor.offset_x(-offset);
self.column_offset -= offset;
}
#[inline]
pub(crate) fn push_cursor(&mut self) {
self.cursor_stack.push((
self.pcursor,
self.cursor,
self.pline_height,
self.line_height,
));
}
#[inline]
pub(crate) fn pop_cursor(&mut self) {
let (pcursor, cursor, pline_height, line_height) =
self.cursor_stack.pop().unwrap_or_default();
self.pcursor = pcursor;
self.cursor = cursor;
self.pline_height = pline_height;
self.line_height = line_height;
}
#[inline]
pub(crate) fn mouse_pos(&self) -> Point<i32> {
let mut pos = self.mouse.pos;
if let Some(offset) = self.mouse_offset {
pos.offset(-offset);
}
pos
}
#[inline]
pub(crate) fn pmouse_pos(&self) -> Point<i32> {
let mut pos = self.pmouse.pos;
if let Some(offset) = self.mouse_offset {
pos.offset(-offset);
}
pos
}
#[inline]
#[must_use]
pub(crate) fn mouse_pressed(&self) -> bool {
self.mouse.is_pressed()
}
#[inline]
#[must_use]
pub(crate) fn mouse_clicked(&self, btn: Mouse) -> bool {
self.mouse.was_clicked(btn)
}
#[inline]
#[must_use]
pub(crate) fn mouse_dbl_clicked(&self, btn: Mouse) -> bool {
self.mouse.was_dbl_clicked(btn)
}
#[inline]
#[must_use]
pub(crate) fn mouse_down(&self, btn: Mouse) -> bool {
self.mouse.is_down(btn)
}
#[inline]
#[must_use]
pub(crate) const fn mouse_buttons(&self) -> &HashSet<Mouse> {
self.mouse.pressed()
}
#[inline]
#[must_use]
pub(crate) fn key_pressed(&self) -> bool {
self.keys.is_pressed()
}
#[inline]
#[must_use]
pub(crate) fn key_down(&self, key: Key) -> bool {
self.keys.is_down(key)
}
#[inline]
#[must_use]
pub(crate) const fn keys(&self) -> &HashSet<Key> {
self.keys.pressed()
}
#[inline]
#[must_use]
pub(crate) const fn keymod_down(&self, keymod: KeyMod) -> bool {
self.keys.mod_down(keymod)
}
#[inline]
pub(crate) const fn keymod(&self) -> &KeyMod {
self.keys.keymod()
}
#[inline]
pub(crate) fn offset_mouse<P: Into<Point<i32>>>(&mut self, offset: P) {
self.mouse_offset = Some(offset.into());
}
#[inline]
pub(crate) fn clear_mouse_offset(&mut self) {
self.mouse_offset = None;
}
#[inline]
#[must_use]
pub(crate) fn is_active(&self, id: ElementId) -> bool {
!self.disabled && matches!(self.active, Some(el) if el == id)
}
#[inline]
#[must_use]
pub(crate) const fn has_active(&self) -> bool {
self.active.is_some()
}
#[inline]
pub(crate) fn set_active(&mut self, id: ElementId) {
self.active = Some(id);
}
#[inline]
pub(crate) fn clear_active(&mut self) {
self.active = None;
}
#[inline]
#[must_use]
pub(crate) fn is_hovered(&self, id: ElementId) -> bool {
matches!(self.hovered, Some(el) if el == id)
}
#[inline]
#[must_use]
pub(crate) const fn has_hover(&self) -> bool {
self.hovered.is_some()
}
#[inline]
pub(crate) fn hover(&mut self, id: ElementId) {
self.hovered = Some(id);
if !self.has_active() && self.mouse.is_down(Mouse::Left) {
self.set_active(id);
}
}
#[inline]
pub(crate) fn clear_hovered(&mut self) {
self.hovered = None;
}
#[inline]
pub(crate) fn try_hover<S: Contains<Point<i32>>>(&mut self, id: ElementId, shape: &S) -> bool {
if !self.has_hover() && !self.disabled && shape.contains(self.mouse_pos()) {
self.hover(id);
}
self.is_hovered(id)
}
#[inline]
#[must_use]
pub(crate) fn is_focused(&self, id: ElementId) -> bool {
!self.disabled && matches!(self.focused, Some(el) if el == id)
}
#[inline]
#[must_use]
pub(crate) const fn has_focused(&self) -> bool {
self.focused.is_some()
}
#[inline]
pub(crate) fn focus(&mut self, id: ElementId) {
self.focused = Some(id);
}
#[inline]
pub(crate) fn try_focus(&mut self, id: ElementId) -> bool {
if !self.disabled && !self.has_focused() {
self.focus(id);
}
self.is_focused(id)
}
#[inline]
pub(crate) fn blur(&mut self) {
self.focused = Some(ElementId::NONE);
}
#[inline]
#[must_use]
pub(crate) fn is_editing(&self, id: ElementId) -> bool {
!self.disabled && matches!(self.editing, Some(el) if el == id)
}
#[inline]
pub(crate) fn begin_edit(&mut self, id: ElementId) {
self.editing = Some(id);
}
#[inline]
pub(crate) fn end_edit(&mut self) {
self.editing = None;
}
#[inline]
pub(crate) fn disable_focus(&mut self) {
self.focus_enabled = false;
}
#[inline]
pub(crate) fn enable_focus(&mut self) {
self.focus_enabled = true;
}
#[inline]
pub(crate) fn handle_focus(&mut self, id: ElementId) {
if !self.focus_enabled {
return;
}
let active = self.is_active(id);
let hovered = self.is_hovered(id);
let focused = self.is_focused(id);
if self.keys.was_entered(Key::Tab) {
let none_focused = self.focused == Some(ElementId::NONE);
if none_focused || focused {
if self.keys.mod_down(KeyMod::SHIFT) {
self.focused = self.last_focusable;
self.clear_entered();
} else if focused {
self.focused = None;
self.clear_entered();
} else if none_focused {
self.focused = Some(id);
self.clear_entered();
}
}
} else if !self.mouse.is_down(Mouse::Left) && active && hovered {
self.focus(id);
} else if focused && self.mouse.is_down(Mouse::Left) && !active && !hovered {
self.blur();
}
self.last_focusable = Some(id);
}
#[inline]
#[must_use]
pub(crate) fn was_clicked(&mut self, id: ElementId) -> bool {
if self.is_focused(id) && self.keys.was_entered(Key::Return) {
self.clear_entered();
true
} else {
!self.mouse.is_down(Mouse::Left) && self.is_hovered(id) && self.is_active(id)
}
}
#[inline]
#[must_use]
pub(crate) const fn key_entered(&self) -> Option<Key> {
self.keys.entered
}
#[inline]
pub(crate) fn clear_entered(&mut self) {
self.keys.typed = None;
self.keys.entered = None;
self.mouse.clicked.clear();
self.mouse.xrel = 0;
self.mouse.yrel = 0;
}
#[inline]
pub(crate) fn scroll(&self, id: ElementId) -> Vector<i32> {
self.elements
.peek(&id)
.map_or_else(Vector::default, |state| state.scroll)
}
#[inline]
pub(crate) fn set_scroll(&mut self, id: ElementId, scroll: Vector<i32>) {
if let Some(state) = self.elements.get_mut(&id) {
state.scroll = scroll;
} else {
self.elements.put(
id,
ElementState {
scroll,
..ElementState::default()
},
);
}
}
#[inline]
#[must_use]
pub(crate) fn text_edit<S>(&mut self, id: ElementId, initial_text: S) -> String
where
S: Into<String>,
{
self.elements.get_mut(&id).map_or_else(
|| initial_text.into(),
|state| mem::take(&mut state.text_edit),
)
}
#[inline]
pub(crate) fn set_text_edit(&mut self, id: ElementId, text_edit: String) {
if let Some(state) = self.elements.get_mut(&id) {
state.text_edit = text_edit;
} else {
self.elements.put(
id,
ElementState {
text_edit,
..ElementState::default()
},
);
}
}
#[inline]
#[must_use]
pub(crate) fn parse_text_edit<T>(&mut self, id: ElementId, default: T) -> T
where
T: FromStr + Copy,
<T as FromStr>::Err: Error + Sync + Send + 'static,
{
self.elements
.pop(&id)
.map_or(default, |state| state.text_edit.parse().unwrap_or(default))
}
#[inline]
#[must_use]
pub(crate) fn expanded(&mut self, id: ElementId) -> bool {
self.elements
.get_mut(&id)
.map_or(false, |state| state.expanded)
}
#[inline]
pub(crate) fn set_expanded(&mut self, id: ElementId, expanded: bool) {
if let Some(state) = self.elements.get_mut(&id) {
state.expanded = expanded;
} else {
self.elements.put(
id,
ElementState {
expanded,
..ElementState::default()
},
);
}
}
#[inline]
#[must_use]
pub(crate) fn last_width(&self) -> i32 {
self.last_size.map(|s| s.width()).unwrap_or_default()
}
}
impl PixState {
#[inline]
pub fn push_id<I>(&mut self, id: I)
where
I: TryInto<u64>,
{
self.ui.id_stack.push(id.try_into().unwrap_or(1));
}
#[inline]
pub fn pop_id(&mut self) {
self.ui.id_stack.pop();
}
#[inline]
pub const fn cursor_pos(&self) -> Point<i32> {
self.ui.cursor()
}
#[inline]
pub fn set_cursor_pos<P: Into<Point<i32>>>(&mut self, cursor: P) {
self.ui.set_cursor(cursor.into());
}
#[inline]
pub fn set_column_offset(&mut self, offset: i32) {
self.ui.set_column_offset(offset);
}
#[inline]
pub fn reset_column_offset(&mut self) {
self.ui.reset_column_offset();
}
#[inline]
#[must_use]
pub fn hovered(&self) -> bool {
self.ui.last_size.map_or(false, |rect| {
!self.ui.disabled && rect.contains(self.mouse_pos())
})
}
#[inline]
#[must_use]
pub fn clicked(&self) -> bool {
self.ui.last_size.map_or(false, |rect| {
!self.ui.disabled && self.mouse_clicked(Mouse::Left) && rect.contains(self.mouse_pos())
})
}
#[inline]
#[must_use]
pub fn dbl_clicked(&self) -> bool {
self.ui.last_size.map_or(false, |rect| {
!self.ui.disabled
&& self.mouse_clicked(Mouse::Left)
&& self.mouse_dbl_clicked(Mouse::Left)
&& rect.contains(self.mouse_pos())
})
}
}
impl PixState {
#[inline]
pub(crate) fn advance_cursor<S: Into<Point<i32>>>(&mut self, size: S) {
let size = size.into();
let pos = self.ui.cursor;
let padx = self.theme.spacing.frame_pad.x();
let pady = self.theme.spacing.item_pad.y();
let offset_x = self.ui.column_offset;
self.ui.pcursor = point![pos.x() + size.x(), pos.y()];
if self.settings.rect_mode == RectMode::Center {
self.ui.pcursor.offset(-size / 2);
}
if cfg!(feature = "debug_ui") {
self.push();
self.fill(None);
self.stroke(Color::RED);
let _result = self.rect(rect![pos, size.x(), size.y()]);
self.fill(Color::BLUE);
let _result = self.circle(circle![self.ui.pcursor(), 3]);
self.pop();
}
let line_height = self.ui.line_height.max(size.y());
self.ui.cursor = point![padx + offset_x, pos.y() + line_height + pady];
self.ui.pline_height = line_height;
self.ui.line_height = 0;
self.ui.last_size = Some(rect![pos, size.x(), size.y()]);
}
#[inline]
pub(crate) fn get_or_create_texture<R>(
&mut self,
id: ElementId,
src: R,
dst: Rect<i32>,
) -> PixResult<TextureId>
where
R: Into<Option<Rect<i32>>>,
{
let font_id = self.theme.fonts.body.id();
let font_size = self.theme.font_size;
if let Some(texture) = self
.ui
.textures
.iter_mut()
.find(|t| t.element_id == id && t.font_id == font_id && t.font_size == font_size)
{
texture.visible = true;
texture.dst = Some(dst);
Ok(texture.id)
} else {
let texture_id =
self.create_texture(dst.width() as u32, dst.height() as u32, PixelFormat::Rgba)?;
self.ui.textures.push(Texture::new(
texture_id,
id,
src.into(),
Some(dst),
font_id,
font_size,
));
Ok(texture_id)
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ElementState {
scroll: Vector<i32>,
text_edit: String,
current_tab: usize,
expanded: bool,
}