use std::{sync::Arc, time::Instant}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton}; use crate::{ gui::{DrawInfo, EventInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemChildren}, gui_text::Label, }; /* Some basic structs to use everywhere. Mostly containers for other GuiElems. */ /// A simple container for zero, one, or multiple child GuiElems. Can optionally fill the background with a color. pub struct Panel { config: GuiElemCfg, pub children: C, pub background: Option, } impl Panel { pub fn new(config: GuiElemCfg, children: C) -> Self { Self { config, children, background: None, } } pub fn with_background(config: GuiElemCfg, children: C, background: Color) -> Self { Self { config, children, background: Some(background), } } } impl GuiElem for Panel { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { self.children.iter() } fn any(&self) -> &dyn std::any::Any { self } fn any_mut(&mut self) -> &mut dyn std::any::Any { self } fn elem(&self) -> &dyn GuiElem { self } fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { if let Some(c) = self.background { g.draw_rectangle(info.pos.clone(), c); } } } #[derive(Clone)] pub struct Square { config: GuiElemCfg, pub inner: T, } #[allow(unused)] impl Square { pub fn new(mut config: GuiElemCfg, inner: T) -> Self { config.redraw = true; Self { config, inner } } } impl GuiElem for Square { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { Box::new([self.inner.elem_mut()].into_iter()) } fn any(&self) -> &dyn std::any::Any { self } fn any_mut(&mut self) -> &mut dyn std::any::Any { self } fn elem(&self) -> &dyn GuiElem { self } fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { if info.pos.size() != self.config.pixel_pos.size() { self.config.redraw = true; } if self.config.redraw { self.config.redraw = false; if info.pos.width() > info.pos.height() { let w = 0.5 * info.pos.height() / info.pos.width(); self.inner.config_mut().pos = Rectangle::from_tuples((0.5 - w, 0.0), (0.5 + w, 1.0)); } else { let h = 0.5 * info.pos.width() / info.pos.height(); self.inner.config_mut().pos = Rectangle::from_tuples((0.0, 0.5 - h), (1.0, 0.5 + h)); } } } } pub struct ScrollBox { config: GuiElemCfg, pub children: C, pub children_heights: Vec, pub default_size: f32, pub size_unit: ScrollBoxSizeUnit, pub scroll_target: f32, pub scroll_display: f32, /// the y-position of the bottom edge of the last element (i.e. the total height) height_bottom: f32, /// 0.max(height_bottom - 1) max_scroll: f32, last_height_px: f32, mouse_in_scrollbar: bool, mouse_scrolling: bool, mouse_scroll_margin_right: f32, } #[derive(Clone)] #[allow(unused)] pub enum ScrollBoxSizeUnit { Relative, Pixels, } impl ScrollBox { pub fn new( config: GuiElemCfg, size_unit: ScrollBoxSizeUnit, children: C, children_heights: Vec, default_size: f32, ) -> Self { Self { config: config.w_scroll().w_mouse(), children, children_heights, default_size, size_unit, scroll_target: 0.0, scroll_display: 0.0, height_bottom: 0.0, max_scroll: 0.0, last_height_px: 0.0, mouse_in_scrollbar: false, mouse_scrolling: false, mouse_scroll_margin_right: 0.0, } } } impl GuiElem for ScrollBox { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { self.children.iter() } fn draw_rev(&self) -> bool { false } fn any(&self) -> &dyn std::any::Any { self } fn any_mut(&mut self) -> &mut dyn std::any::Any { self } fn elem(&self) -> &dyn GuiElem { self } fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { if self.config.pixel_pos.size() != info.pos.size() { self.config.redraw = true; } // smooth scrolling animation if self.scroll_target > self.max_scroll { self.scroll_target = self.max_scroll; } else if self.scroll_target < 0.0 { self.scroll_target = 0.0; } if self.scroll_target != self.scroll_display { self.config.redraw = true; if info.high_performance { self.scroll_display = self.scroll_target; } else { self.scroll_display = 0.2 * self.scroll_target + 0.8 * self.scroll_display; if (self.scroll_display - self.scroll_target).abs() < 1.0 / info.pos.height() { self.scroll_display = self.scroll_target; } else if let Some(h) = &info.helper { h.request_redraw(); } } } // recalculate positions if self.config.redraw { // adjust height vector length if necessary if self.children_heights.len() != self.children.len() { let target = self.children.len(); while self.children_heights.len() < target { self.children_heights.push(self.default_size); } while self.children_heights.len() > target { self.children_heights.pop(); } } // self.mouse_scroll_margin_right = info.line_height * 0.2; let max_x = 1.0 - self.mouse_scroll_margin_right / info.pos.width(); self.config.redraw = false; let mut y_pos = -self.scroll_display; for (e, h) in self.children.iter().zip(self.children_heights.iter()) { let h_rel = self.size_unit.to_rel(*h, info.pos.height()); let y_rel = self.size_unit.to_rel(y_pos, info.pos.height()); if y_rel + h_rel >= 0.0 && y_rel <= 1.0 { let cfg = e.config_mut(); cfg.enabled = if info.high_performance { y_rel >= 0.0 && y_rel + h_rel <= 1.0 } else { true }; cfg.pos = Rectangle::new( Vec2::new(cfg.pos.top_left().x, 0.0f32.max(y_rel)), Vec2::new( cfg.pos.bottom_right().x.min(max_x), 1.0f32.min(y_rel + h_rel), ), ); } else { e.config_mut().enabled = false; } y_pos += *h; } self.height_bottom = y_pos + self.scroll_display; self.max_scroll = 0.0f32.max(self.height_bottom - self.size_unit.from_rel(0.75, info.pos.height())); } // scroll bar self.mouse_in_scrollbar = info.mouse_pos.y >= info.pos.top_left().y && info.mouse_pos.y <= info.pos.bottom_right().y && info.mouse_pos.x <= info.pos.bottom_right().x && info.mouse_pos.x >= (info.pos.bottom_right().x - self.mouse_scroll_margin_right); if self.mouse_scrolling { self.scroll_target = (self.max_scroll * (info.mouse_pos.y - info.pos.top_left().y) / info.pos.height()) .max(0.0) .min(self.max_scroll); } if self.mouse_in_scrollbar || self.mouse_scrolling || (self.scroll_display - self.scroll_target).abs() > self.size_unit.from_abs(1.0, info.pos.height()) { let y1 = info.pos.top_left().y + info.pos.height() * self.scroll_display.min(self.scroll_target) / self.max_scroll - 1.0; let y2 = info.pos.top_left().y + info.pos.height() * self.scroll_display.max(self.scroll_target) / self.max_scroll + 1.0; g.draw_rectangle( Rectangle::from_tuples( ( info.pos.bottom_right().x - self.mouse_scroll_margin_right, y1, ), (info.pos.bottom_right().x, y2), ), Color::WHITE, ); } } fn mouse_wheel(&mut self, e: &mut EventInfo, diff: f32) -> Vec { let nst = (self.scroll_target - self.size_unit.from_abs(diff as f32, self.last_height_px)) .max(0.0); // only take the event if this would actually scroll, and only scroll if we can actually take the event if nst != self.scroll_target && e.take() { self.scroll_target = nst; } Vec::with_capacity(0) } fn mouse_down(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec { if button == MouseButton::Left && self.mouse_in_scrollbar && e.take() { self.mouse_scrolling = true; } vec![] } fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec { if button == MouseButton::Left { if self.mouse_scrolling { e.take(); } self.mouse_scrolling = false; } vec![] } } impl ScrollBoxSizeUnit { fn to_rel(&self, val: f32, draw_height: f32) -> f32 { match self { Self::Relative => val, Self::Pixels => val / draw_height, } } fn from_rel(&self, val: f32, draw_height: f32) -> f32 { match self { Self::Relative => val, Self::Pixels => val * draw_height, } } fn from_abs(&self, val: f32, draw_height: f32) -> f32 { match self { Self::Relative => val / draw_height, Self::Pixels => val, } } } pub struct Button { config: GuiElemCfg, pub children: C, action: Arc Vec + 'static>, } impl Button { /// automatically adds w_mouse to config pub fn new Vec + 'static>( config: GuiElemCfg, action: F, children: C, ) -> Self { Self { config: config.w_mouse().w_keyboard_focus(), children, action: Arc::new(action), } } } impl GuiElem for Button { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { self.children.iter() } fn any(&self) -> &dyn std::any::Any { self } fn any_mut(&mut self) -> &mut dyn std::any::Any { self } fn elem(&self) -> &dyn GuiElem { self } fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn mouse_pressed(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec { if button == MouseButton::Left && e.take() { (self.action.clone())(self) } else { vec![] } } fn key_focus( &mut self, e: &mut EventInfo, _modifiers: speedy2d::window::ModifiersState, down: bool, key: Option, _scan: speedy2d::window::KeyScancode, ) -> Vec { if !down && matches!( key, Some( speedy2d::window::VirtualKeyCode::Return | speedy2d::window::VirtualKeyCode::NumpadEnter, ) ) && e.take() { (self.action.clone())(self) } else { vec![] } } fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { let mouse_down = self.config.mouse_down.0; let contains = info.pos.contains(info.mouse_pos); g.draw_rectangle( info.pos.clone(), if mouse_down && contains { Color::from_rgb(0.25, 0.25, 0.25) } else if contains || mouse_down { Color::from_rgb(0.15, 0.15, 0.15) } else { Color::from_rgb(0.1, 0.1, 0.1) }, ); if info.has_keyboard_focus { g.draw_line( *info.pos.top_left(), info.pos.top_right(), 2.0, Color::WHITE, ); g.draw_line( *info.pos.top_left(), info.pos.bottom_left(), 2.0, Color::WHITE, ); g.draw_line( info.pos.top_right(), *info.pos.bottom_right(), 2.0, Color::WHITE, ); g.draw_line( info.pos.bottom_left(), *info.pos.bottom_right(), 2.0, Color::WHITE, ); } } } pub struct Slider { pub config: GuiElemCfg, pub children: Vec>, pub slider_pos: Rectangle, pub min: f64, pub max: f64, pub val: f64, val_changed: bool, pub val_changed_subs: Vec, /// if true, the display should be visible. pub display: bool, /// if Some, the display is in a transition period. /// you can set this to None to indicate that the transition has finished, but this is not required. pub display_since: Option, pub on_update: Arc, } #[allow(unused)] impl Slider { /// returns true if the value of the slider has changed since the last time this function was called. /// this is usually used by the closure responsible for directly handling updates. if you wish to check for changes /// from outside, push a `false` to `val_changed_subs` and remember your index. /// when the value changes, this will be set to `true`. don't forget to reset it to `false` again if you find it set to `true`, /// or your code will run every time. pub fn val_changed(&mut self) -> bool { if self.val_changed { self.val_changed = false; true } else { false } } pub fn val_changed_peek(&self) -> bool { self.val_changed } pub fn new( config: GuiElemCfg, slider_pos: Rectangle, min: f64, max: f64, val: f64, children: Vec>, on_update: F, ) -> Self { Self { config: config.w_mouse().w_scroll(), children, slider_pos, min, max, val, val_changed: true, val_changed_subs: vec![], display: false, display_since: None, on_update: Arc::new(on_update), } } pub fn new_labeled( config: GuiElemCfg, min: f64, max: f64, val: f64, mktext: F, ) -> Self { Self::new( config, Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)), min, max, val, vec![Box::new(Label::new( GuiElemCfg::default(), String::new(), Color::WHITE, // Some(Color::from_int_rgba(0, 0, 0, 150)), None, Vec2::new(0.5, 1.0), ))], move |s, i| { if s.display || s.display_since.is_some() { let mut label = s.children.pop().unwrap(); if let Some(l) = label.any_mut().downcast_mut::