use std::{fmt::Display, rc::Rc, sync::Arc}; use musicdb_lib::data::CoverId; use speedy2d::{ color::Color, dimen::Vec2, font::{FormattedTextBlock, TextLayout, TextOptions}, image::ImageHandle, shape::Rectangle, window::{ModifiersState, MouseButton}, }; use crate::gui::{GuiAction, GuiElem, GuiElemCfg, GuiServerImage}; /* Some basic structs to use everywhere, except they are all text-related. */ #[derive(Clone)] pub struct Label { config: GuiElemCfg, pub content: Content, pub pos: Vec2, } #[derive(Clone)] pub struct Content { text: String, color: Color, background: Option, formatted: Option>, } #[allow(unused)] impl Content { pub fn new(text: String, color: Color) -> Self { Self { text: if text.starts_with(' ') { text.replacen(' ', "\u{00A0}", 1) } else { text }, color, background: None, formatted: None, } } pub fn get_text(&self) -> &String { &self.text } pub fn get_color(&self) -> &Color { &self.color } /// causes text layout reset pub fn text(&mut self) -> &mut String { self.formatted = None; &mut self.text } pub fn color(&mut self) -> &mut Color { &mut self.color } /// returns true if the text needs to be redrawn, probably because it was changed. pub fn will_redraw(&self) -> bool { self.formatted.is_none() } } impl Label { pub fn new( config: GuiElemCfg, text: String, color: Color, background: Option, pos: Vec2, ) -> Self { Self { config, content: Content { text, color, background, formatted: None, }, pos, } } } impl GuiElem for Label { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { Box::new([].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 crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { if self.config.pixel_pos.size() != info.pos.size() { // resize self.content.formatted = None; } let text = if let Some(text) = &self.content.formatted { text } else { let l = info .font .layout_text(&self.content.text, 1.0, TextOptions::new()); let l = info.font.layout_text( &self.content.text, (info.pos.width() / l.width()).min(info.pos.height() / l.height()), TextOptions::new(), ); self.content.formatted = Some(l); self.content.formatted.as_ref().unwrap() }; let top_left = Vec2::new( info.pos.top_left().x + self.pos.x * (info.pos.width() - text.width()), info.pos.top_left().y + self.pos.y * (info.pos.height() - text.height()), ); if let Some(bg) = self.content.background { g.draw_rectangle( Rectangle::new( top_left, Vec2::new(top_left.x + text.width(), top_left.y + text.height()), ), bg, ); } g.draw_text(top_left, self.content.color, text); } } // TODO! this, but requires keyboard events first /// a single-line text field for users to type text into. pub struct TextField { config: GuiElemCfg, pub c_input: Label, pub c_hint: Label, pub on_changed: Option>, pub on_changed_mut: Option>, } impl TextField { pub fn new(config: GuiElemCfg, hint: String, color_hint: Color, color_input: Color) -> Self { Self::new_adv(config, String::new(), hint, color_hint, color_input) } pub fn new_adv( config: GuiElemCfg, text: String, hint: String, color_hint: Color, color_input: Color, ) -> Self { let text_is_empty = text.is_empty(); Self { config: config.w_mouse().w_keyboard_focus(), c_input: Label::new( GuiElemCfg::default(), text, color_input, None, Vec2::new(0.0, 0.5), ), c_hint: Label::new( if text_is_empty { GuiElemCfg::default() } else { GuiElemCfg::default().disabled() }, hint, color_hint, None, Vec2::new(0.0, 0.5), ), on_changed: None, on_changed_mut: None, } } } impl GuiElem for TextField { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { Box::new([self.c_input.elem_mut(), self.c_hint.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 crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { let (t, c) = if info.has_keyboard_focus { (3.0, Color::WHITE) } else { (1.0, Color::GRAY) }; g.draw_line(info.pos.top_left(), info.pos.top_right(), t, c); g.draw_line(info.pos.bottom_left(), info.pos.bottom_right(), t, c); g.draw_line(info.pos.top_left(), info.pos.bottom_left(), t, c); g.draw_line(info.pos.top_right(), info.pos.bottom_right(), t, c); } fn mouse_pressed(&mut self, _button: MouseButton) -> Vec { self.config.request_keyboard_focus = true; vec![GuiAction::ResetKeyboardFocus] } fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec { if !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && !key.is_control() { let content = &mut self.c_input.content; let was_empty = content.get_text().is_empty(); content.text().push(key); if let Some(f) = &mut self.on_changed { f(content.get_text()); } if let Some(mut f) = self.on_changed_mut.take() { let text = content.get_text().clone(); f(self, text); self.on_changed_mut = Some(f); } if was_empty { self.c_hint.config_mut().enabled = false; } } vec![] } fn key_focus( &mut self, modifiers: ModifiersState, down: bool, key: Option, _scan: speedy2d::window::KeyScancode, ) -> Vec { if down && !(modifiers.alt() || modifiers.logo()) && key == Some(speedy2d::window::VirtualKeyCode::Backspace) { let content = &mut self.c_input.content; if !content.get_text().is_empty() { if modifiers.ctrl() { for s in [true, false, true] { while !content.get_text().is_empty() && content.get_text().ends_with(' ') == s { content.text().pop(); } } } else { content.text().pop(); } let is_now_empty = content.get_text().is_empty(); if let Some(f) = &mut self.on_changed { f(content.get_text()); } if let Some(mut f) = self.on_changed_mut.take() { let text = content.get_text().clone(); f(self, text); self.on_changed_mut = Some(f); } if is_now_empty { self.c_hint.config_mut().enabled = true; } } } vec![] } } #[derive(Clone)] pub enum AdvancedContent { Text(Content), Image { source: ImageSource, handle: Option>, }, } #[derive(Clone)] pub enum ImageSource { Cover(CoverId), CustomFile(String), } impl AdvancedContent { pub fn will_redraw(&self) -> bool { match self { Self::Text(c) => c.will_redraw(), Self::Image { source: _, handle } => handle.is_none(), } } } impl Display for AdvancedContent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Text(c) => write!(f, "{}", c.text), Self::Image { .. } => Ok(()), } } } /// More advanced version of `Label`. /// Allows stringing together multiple `Content`s in one line. pub struct AdvancedLabel { config: GuiElemCfg, children: Vec>, /// 0.0 => align to top/left /// 0.5 => center /// 1.0 => align to bottom/right pub align: Vec2, /// (Content, Size-Scale, Height) /// Size-Scale and Height should default to 1.0. pub content: Vec>, /// the position from where content drawing starts. /// recalculated when layouting is performed. content_pos: Vec2, } impl AdvancedLabel { pub fn new( config: GuiElemCfg, align: Vec2, content: Vec>, ) -> Self { Self { config, children: vec![], align, content, content_pos: Vec2::ZERO, } } } impl GuiElem for AdvancedLabel { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { Box::new(self.children.iter_mut().map(|v| v.elem_mut())) } 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 crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { if self.config.redraw || self.config.pixel_pos.size() != info.pos.size() || self .content .iter() .any(|v| v.iter().any(|(c, _, _)| c.will_redraw())) { self.config.redraw = false; let mut max_len = 0.0; let mut total_height = 0.0; for line in &self.content { let mut len = 0.0; let mut height = 0.0; for (c, scale, _) in line { match c { AdvancedContent::Text(c) => { let size = info .font .layout_text(&c.text, 1.0, TextOptions::new()) .size(); len += size.x * scale; if size.y * scale > height { height = size.y * scale; } } AdvancedContent::Image { source, handle } => {} } } for (c, scale, _) in line { match c { AdvancedContent::Text(_) => {} AdvancedContent::Image { source, handle } => { if let Some(Some(handle)) = handle { let size = handle.size().into_f32(); len += height * size.x / size.y; } } } } if len > max_len { max_len = len; } total_height += height; } if max_len > 0.0 && total_height > 0.0 { let scale1 = info.pos.width() / max_len; let scale2 = info.pos.height() / total_height; let scale; self.content_pos = if scale1 < scale2 { // use all available width scale = scale1; Vec2::new( 0.0, (info.pos.height() - (total_height * scale)) * self.align.y, ) } else { // use all available height scale = scale2; Vec2::new((info.pos.width() - (max_len * scale)) * self.align.x, 0.0) }; for line in &mut self.content { for (c, s, _) in line { match c { AdvancedContent::Text(c) => { c.formatted = Some(info.font.layout_text( &c.text, scale * (*s), TextOptions::new(), )); } AdvancedContent::Image { source, handle } => { if handle.is_none() { match source { ImageSource::Cover(id) => { if let Some(img) = info.covers.get_mut(&id) { if let Some(img) = img.get_init(g) { *handle = Some(Some(img)); } else { match img { GuiServerImage::Loading(_) => {} GuiServerImage::Loaded(_) => {} GuiServerImage::Error => { *handle = Some(None) } } } } else { info.covers.insert( *id, GuiServerImage::new_cover( *id, Arc::clone(&info.get_con), ), ); } } ImageSource::CustomFile(path) => { if let Some(img) = info.custom_images.get_mut(path) { if let Some(img) = img.get_init(g) { *handle = Some(Some(img)); } else { match img { GuiServerImage::Loading(_) => {} GuiServerImage::Loaded(_) => {} GuiServerImage::Error => { *handle = Some(None) } } } } else { info.custom_images.insert( path.clone(), GuiServerImage::new_custom_file( path.clone(), Arc::clone(&info.get_con), ), ); } } } } } } } } } } let pos_x_start = info.pos.top_left().x + self.content_pos.x; let mut pos_y = info.pos.top_left().y + self.content_pos.y; for line in &self.content { let mut pos_x = pos_x_start; let height_div_by = line .iter() .map(|(_, scale, _)| *scale) .reduce(f32::max) .unwrap_or(1.0); let line_height = line .iter() .filter_map(|(v, _, _)| { if let AdvancedContent::Text(c) = v { Some(c) } else { None } }) .filter_map(|v| v.formatted.as_ref()) .map(|f| f.height()) .reduce(f32::max) .unwrap_or(0.0); for (c, scale, placement_height) in line { // not super accurate, but pretty good let rel_scale = f32::min(1.0, scale / height_div_by); match c { AdvancedContent::Text(c) => { if let Some(f) = &c.formatted { let y = pos_y + (line_height - f.height()) * placement_height; g.draw_text(Vec2::new(pos_x, y), c.color, f); pos_x += f.width(); } } AdvancedContent::Image { source: _, handle } => { if let Some(Some(handle)) = handle { let size = handle.size().into_f32(); let h = line_height * rel_scale; let w = h * size.x / size.y; let y = pos_y + (line_height - h) * placement_height; g.draw_rectangle_image( Rectangle::from_tuples((pos_x, y), (pos_x + w, y + h)), handle, ); pos_x += w; } } } } pos_y += line_height; } } }