mirror of
				https://github.com/Dummi26/musicdb.git
				synced 2025-10-31 20:16:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			555 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Rust
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			555 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Rust
		
	
	
		
			Executable File
		
	
	
	
	
| 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<Color>,
 | |
|     formatted: Option<Rc<FormattedTextBlock>>,
 | |
| }
 | |
| 
 | |
| #[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<Color>,
 | |
|         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<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
 | |
|         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<Box<dyn FnMut(&str)>>,
 | |
|     pub on_changed_mut: Option<Box<dyn FnMut(&mut Self, String)>>,
 | |
| }
 | |
| 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<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
 | |
|         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<GuiAction> {
 | |
|         self.config.request_keyboard_focus = true;
 | |
|         vec![GuiAction::ResetKeyboardFocus]
 | |
|     }
 | |
|     fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> {
 | |
|         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<speedy2d::window::VirtualKeyCode>,
 | |
|         _scan: speedy2d::window::KeyScancode,
 | |
|     ) -> Vec<GuiAction> {
 | |
|         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<Option<ImageHandle>>,
 | |
|     },
 | |
| }
 | |
| #[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<Box<dyn GuiElem>>,
 | |
|     /// 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<Vec<(AdvancedContent, f32, f32)>>,
 | |
|     /// 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<Vec<(AdvancedContent, f32, f32)>>,
 | |
|     ) -> 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<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
 | |
|         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;
 | |
|         }
 | |
|     }
 | |
| }
 | 
