use std::collections::{HashSet, VecDeque};
use std::hash::Hash;
use ggez::audio::{SoundSource, Source};
use ggez::winit::event::VirtualKeyCode;
use ggez::{
glam::Vec2,
graphics::{Canvas, Rect},
Context, GameResult,
};
mod ui_element;
pub use ui_element::UiContainer;
pub use ui_element::UiContent;
pub mod basic;
pub mod containers;
mod layout;
pub use layout::Alignment;
pub use layout::Layout;
pub use layout::Size;
mod visuals;
use tinyvec::TinyVec;
pub use visuals::Visuals;
mod transition;
pub use transition::Transition;
mod draw_cache;
use draw_cache::DrawCache;
mod message;
pub use message::UiMessage;
mod ui_element_builder;
pub use ui_element_builder::UiElementBuilder;
mod ui_draw_param;
pub use ui_draw_param::UiDrawParam;
pub struct UiElement<T: Copy + Eq + Hash> {
layout: Layout,
visuals: Visuals,
hover_visuals: Option<Visuals>,
trigger_sound: Option<Source>,
id: u32,
draw_cache: DrawCache,
pub content: Box<dyn UiContent<T>>,
tooltip: Option<Box<UiElement<T>>>,
transitions: VecDeque<Transition<T>>,
keys: TinyVec<[Option<VirtualKeyCode>; 2]>,
message_handler: MessageHandler<T>,
}
type MessageHandler<T> = Box<dyn Fn(&HashSet<UiMessage<T>>, Layout, &mut VecDeque<Transition<T>>)>;
impl<T: Copy + Eq + Hash> std::fmt::Debug for UiElement<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UiElement")
.field("layout", &self.layout)
.field("visuals", &self.visuals)
.field("hover_visuals", &self.hover_visuals)
.field("trigger_sound", &self.trigger_sound)
.field("id", &self.id)
.field("draw_cache", &self.draw_cache)
.field("tooltip", &self.tooltip)
.field("keys", &self.keys)
.finish()
}
}
impl<T: Copy + Eq + Hash> UiElement<T> {
pub fn new<E: UiContent<T> + 'static>(id: u32, content: E) -> Self {
Self {
layout: Layout::default(),
visuals: Visuals::default(),
hover_visuals: None,
trigger_sound: None,
id,
draw_cache: DrawCache::default(),
content: Box::new(content),
tooltip: None,
transitions: VecDeque::new(),
keys: TinyVec::new(),
message_handler: Box::new(|_messages, _layout, _transition_queue| {}),
}
}
pub fn add_element(&mut self, id: u32, element: UiElement<T>) -> Option<UiElement<T>> {
match self.content.container_mut() {
Some(cont) => {
if self.id == id {
cont.add(element);
return None;
};
let mut element_option = Some(element);
for child in cont.get_children_mut().iter_mut() {
if let Some(element) = element_option {
element_option = child.add_element(id, element);
} else {
break;
}
}
element_option
}
None => Some(element),
}
}
pub fn remove_elements(&mut self, id: u32) {
if let Some(cont) = self.content.container_mut() {
cont.remove_id(id);
for child in cont.get_children_mut() {
child.remove_elements(id);
}
}
}
pub fn get_id(&self) -> u32 {
self.id
}
pub fn get_layout(&self) -> Layout {
self.layout
}
pub fn update(
&mut self,
ctx: &ggez::Context,
extern_messages: impl Into<Option<HashSet<UiMessage<T>>>>,
) -> HashSet<UiMessage<T>> {
let intern_messages = self.collect_messages(ctx);
let all_messages = match extern_messages.into() {
None => intern_messages.clone(),
Some(extern_messages) => intern_messages.union(&extern_messages).copied().collect(),
};
self.distribute_messages(&all_messages).expect("Something went wrong delivering or executing messages. Probably you wrote a bad handler function.");
intern_messages
}
pub(crate) fn expired(&self) -> bool {
self.content.expired()
}
pub fn manage_messages(
&mut self,
ctx: &ggez::Context,
extern_messages: impl Into<Option<HashSet<UiMessage<T>>>>,
) -> HashSet<UiMessage<T>> {
self.update(ctx, extern_messages)
}
fn collect_messages(&self, ctx: &Context) -> HashSet<UiMessage<T>> {
let mut res: HashSet<UiMessage<T>> = HashSet::new();
if self.id != 0
&& match self.draw_cache {
DrawCache::Invalid => false,
DrawCache::Valid {
outer,
inner: _,
target: _,
} => outer.contains(ctx.mouse.position()),
}
{
if ctx
.mouse
.button_just_pressed(ggez::event::MouseButton::Left)
{
res.insert(UiMessage::Clicked(self.id));
res.insert(UiMessage::Triggered(self.id));
if let Some(sound) = &self.trigger_sound {
if sound.play_later().is_err() && cfg!(debug_assertions) {
println!("[ERROR] Failed to play sound.");
}
}
}
if ctx
.mouse
.button_just_pressed(ggez::event::MouseButton::Right)
{
res.insert(UiMessage::ClickedRight(self.id));
}
}
if self.id != 0
&& self.keys.iter().any(|key_opt| {
if let Some(key) = key_opt {
ctx.keyboard.is_key_just_pressed(*key)
} else {
false
}
})
{
res.insert(UiMessage::PressedKey(self.id));
res.insert(UiMessage::Triggered(self.id));
}
if let Some(cont) = self.content.container() {
for child in cont.get_children() {
res.extend(child.collect_messages(ctx));
}
}
res
}
fn distribute_messages(&mut self, messages: &HashSet<UiMessage<T>>) -> GameResult {
(self.message_handler)(messages, self.layout, &mut self.transitions);
if let Some(cont) = self.content.container_mut() {
for child in cont.get_children_mut() {
child.distribute_messages(messages)?;
}
cont.remove_expired();
}
Ok(())
}
pub fn add_transition(&mut self, transition: Transition<T>) {
self.transitions.push_back(transition);
}
fn progress_transitions(&mut self, ctx: &Context) {
if !self.transitions.is_empty() && self.transitions[0].progress(ctx.time.delta()) {
let trans = self.transitions.pop_front().expect(
"Transitions did not contain a first element despite being not empty 2 lines ago.",
);
if let Some(layout) = trans.new_layout {
self.layout = layout;
self.draw_cache = DrawCache::Invalid;
}
if let Some(visuals) = trans.new_visuals {
self.visuals = visuals;
}
if let Some(hover_visuals) = trans.new_hover_visuals {
self.hover_visuals = hover_visuals;
}
if let Some(content) = trans.new_content {
self.content = content;
}
if let Some(tooltip) = trans.new_tooltip {
self.tooltip = tooltip;
}
}
}
fn get_current_visual(&self, ctx: &Context, param: UiDrawParam) -> Visuals {
if param.mouse_listen
&& match self.draw_cache {
DrawCache::Invalid => false,
DrawCache::Valid {
outer,
inner: _,
target: _,
} => outer.contains(ctx.mouse.position()),
}
{
let own_vis = if let Some(hover_visuals) = self.hover_visuals {
hover_visuals
} else {
self.visuals
};
if self.transitions.is_empty() {
own_vis
} else {
let trans = &self.transitions[0];
match trans.new_hover_visuals {
Some(vis) => {
let trans_vis = if let Some(hover_visuals) = vis {
hover_visuals
} else {
self.visuals
};
own_vis.average(trans_vis, trans.get_progress_ratio())
}
None => own_vis,
}
}
} else {
if self.transitions.is_empty() {
self.visuals
} else {
let trans = &self.transitions[0];
match trans.new_visuals {
Some(vis) => self.visuals.average(vis, trans.get_progress_ratio()),
None => self.visuals,
}
}
}
}
fn update_draw_cache(&mut self, _ctx: &Context, target: Rect) {
if !self.cache_valid(target) {
let (own_outer, own_inner) = self
.layout
.get_outer_inner_bounds_in_target(&target, self.content_min());
let (outer, inner) = if !self.transitions.is_empty() {
if let Some(new_layout) = self.transitions[0].new_layout {
let (trans_outer, trans_inner) =
new_layout.get_outer_inner_bounds_in_target(&target, self.content_min());
(
transition::average_rect(
&own_outer,
&trans_outer,
self.transitions[0].get_progress_ratio(),
),
transition::average_rect(
&own_inner,
&trans_inner,
self.transitions[0].get_progress_ratio(),
),
)
} else {
(own_outer, own_inner)
}
} else {
(own_outer, own_inner)
};
if outer.w > target.w + 0.01
|| outer.h > target.h + 0.01
|| outer.x < 0.
|| outer.y < 0.
{
if cfg!(test) {
println!(
"Skipped Element due to bounds violation. Outer: {:?}, Target: {:?}",
outer, target
);
}
self.draw_cache = DrawCache::Invalid;
} else {
self.draw_cache = DrawCache::Valid {
outer,
inner,
target,
};
}
}
}
fn cache_valid(&self, target: Rect) -> bool {
let init = match self.draw_cache {
DrawCache::Invalid => false,
DrawCache::Valid {
outer: _,
inner: _,
target: cache_target,
} => cache_target == target,
} && (self.transitions.is_empty()
|| matches!(self.transitions[0].new_layout, None));
match self.content.container() {
Some(cont) => cont
.get_children()
.iter()
.fold(init, |valid, child| valid && child.cache_valid(target)),
None => init,
}
}
pub fn width_range(&self) -> (f32, f32) {
let layout = self.layout;
(
self.content
.container()
.map(|cont| cont.content_width_range())
.unwrap_or((0., f32::INFINITY))
.0
.clamp(layout.x_size.min(), layout.x_size.max())
+ layout.padding.1
+ layout.padding.3,
layout.x_size.max() + layout.padding.1 + layout.padding.3,
)
}
pub fn height_range(&self) -> (f32, f32) {
let layout = self.layout;
(
self.content
.container()
.map(|cont| cont.content_height_range())
.unwrap_or((0., f32::INFINITY))
.0
.clamp(layout.y_size.min(), layout.y_size.max())
+ layout.padding.0
+ layout.padding.2,
layout.y_size.max() + layout.padding.0 + layout.padding.2,
)
}
fn content_min(&self) -> Vec2 {
Vec2 {
x: self
.content
.container()
.map(|cont| cont.content_width_range().0)
.unwrap_or_default(),
y: self
.content
.container()
.map(|cont| cont.content_height_range().0)
.unwrap_or_default(),
}
}
pub(crate) fn draw_to_rectangle(
&mut self,
ctx: &mut Context,
canvas: &mut Canvas,
param: UiDrawParam,
) {
self.progress_transitions(ctx);
self.update_draw_cache(ctx, param.target);
let (outer, inner) = match self.draw_cache {
DrawCache::Invalid => return,
DrawCache::Valid {
outer,
inner,
target: _,
} => (outer, inner),
};
self.get_current_visual(ctx, param)
.draw(ctx, canvas, param.target(outer));
self.content.draw_content(ctx, canvas, param.target(inner));
if param.mouse_listen && outer.contains(ctx.mouse.position()) {
if let Some(tt) = &mut self.tooltip {
let mouse_pos = ctx.mouse.position();
let screen_size = ctx.gfx.window().inner_size();
let tt_size = (tt.width_range().0, tt.height_range().0);
let x = if 2. * inner.x + inner.w > screen_size.width as f32 {
mouse_pos.x - tt_size.0 - 10.
} else {
mouse_pos.x + 10.
}
.clamp(0., screen_size.width as f32 - tt_size.0);
let y = (if 2. * inner.y + inner.h > screen_size.height as f32 {
mouse_pos.y - tt_size.1
} else {
mouse_pos.y
} - 10.)
.max(0.);
tt.draw_to_rectangle(
ctx,
canvas,
param
.target(Rect::new(x, y, tt_size.0, tt_size.1))
.z_level(param.param.z + 1),
);
}
}
}
pub fn draw_to_screen(&mut self, ctx: &mut Context, canvas: &mut Canvas, mouse_listen: bool) {
self.draw_to_rectangle(
ctx,
canvas,
UiDrawParam::default()
.target(Rect::new(
0.,
0.,
ctx.gfx.window().inner_size().width as f32,
ctx.gfx.window().inner_size().height as f32,
))
.mouse_listen(mouse_listen),
);
}
}