use crate::debug_log::{self, log}; use crate::mpd_handler::{InfoFromShared, MPDHandler, MPDHandlerState, MPDPlayState}; use crate::Opt; use ggez::event::{self, EventHandler}; use ggez::graphics::{ self, Color, DrawMode, DrawParam, Drawable, Font, Image, Mesh, MeshBuilder, PxScale, Rect, Text, TextFragment, Transform, }; use ggez::{timer, Context, GameError, GameResult}; use image::io::Reader as ImageReader; use std::io::Cursor; use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::{atomic::Ordering, Arc, RwLockReadGuard}; use std::thread; use std::time::{Duration, Instant}; const POLL_TIME: Duration = Duration::from_millis(333); const INIT_FONT_SIZE_X: f32 = 24.0; const INIT_FONT_SIZE_Y: f32 = 34.0; const TEXT_X_OFFSET: f32 = 0.3; const TEXT_OFFSET_Y_SPACING: f32 = 0.4; const TEXT_HEIGHT_SCALE: f32 = 0.1; const ARTIST_HEIGHT_SCALE: f32 = 0.08; const TIMER_HEIGHT_SCALE: f32 = 0.07; const MIN_WIDTH_RATIO: f32 = 4.0 / 5.0; const INCREASE_AMT: f32 = 6.0 / 5.0; const DECREASE_AMT: f32 = 5.0 / 6.0; fn seconds_to_time(seconds: f64) -> String { let seconds_int: u64 = seconds.floor() as u64; let minutes = seconds_int / 60; let new_seconds: f64 = seconds - (minutes * 60) as f64; let mut result: String; if minutes > 0 { result = minutes.to_string(); result.push(':'); if new_seconds < 10.0 { result.push('0'); } } else { result = String::new(); } result.push_str(&new_seconds.to_string()); let idx_result = result.find('.'); if let Some(idx) = idx_result { result.truncate(idx); } result } #[cfg(not(feature = "unicode_support"))] #[allow(clippy::ptr_arg)] fn string_to_text( string: String, _loaded_fonts: &mut Vec<(PathBuf, Font)>, _ctx: &mut Context, ) -> Text { Text::new(TextFragment::from(string)) } #[cfg(feature = "unicode_support")] fn string_to_text( string: String, loaded_fonts: &mut Vec<(PathBuf, Font)>, ctx: &mut Context, ) -> Text { use super::unicode_support; let mut text = Text::default(); let mut current_fragment = TextFragment::default(); if string.is_ascii() { current_fragment.text = string; text.add(current_fragment); return text; } let find_font = |c: char, loaded_fonts: &mut Vec<(PathBuf, Font)>, ctx: &mut Context| -> Option { for (idx, (path, _)) in loaded_fonts.iter().enumerate() { let result = unicode_support::font_has_char(c, path); if result.is_ok() && result.unwrap() { return Some(idx); } } let find_result = unicode_support::get_matching_font_from_char(c); if let Ok(path) = find_result { let new_font = Font::new(ctx, &path); if let Ok(font) = new_font { loaded_fonts.push((path, font)); return Some(loaded_fonts.len() - 1); } else { log( format!("Failed to load {:?}: {:?}", &path, new_font), debug_log::LogState::Error, debug_log::LogLevel::Error, ); } } else { log( format!("Failed to find font for {}", c), debug_log::LogState::Error, debug_log::LogLevel::Error, ); } None }; let mut prev_is_ascii = true; for c in string.chars() { if c.is_ascii() { if prev_is_ascii { current_fragment.text.push(c); } else { if !current_fragment.text.is_empty() { text.add(current_fragment); current_fragment = Default::default(); } current_fragment.text.push(c); } prev_is_ascii = true; } else { let idx_opt = find_font(c, loaded_fonts, ctx); if prev_is_ascii { if let Some(idx) = idx_opt { if !current_fragment.text.is_empty() { text.add(current_fragment); current_fragment = Default::default(); } let (_, font) = loaded_fonts[idx]; current_fragment.font = Some(font); } current_fragment.text.push(c); } else if let Some(idx) = idx_opt { let font = loaded_fonts[idx].1; if let Some(current_font) = current_fragment.font { if current_font == font { current_fragment.text.push(c); } else { if !current_fragment.text.is_empty() { text.add(current_fragment); current_fragment = Default::default(); } current_fragment.text.push(c); current_fragment.font = Some(font); } } else if current_fragment.text.is_empty() { current_fragment.text.push(c); current_fragment.font = Some(font); } else { text.add(current_fragment); current_fragment = Default::default(); current_fragment.text.push(c); current_fragment.font = Some(font); } } else { if !current_fragment.text.is_empty() && current_fragment.font.is_some() { text.add(current_fragment); current_fragment = Default::default(); } current_fragment.text.push(c); } prev_is_ascii = false; } } if !current_fragment.text.is_empty() { text.add(current_fragment); } text } pub struct MPDDisplay { opts: Opt, mpd_handler: Result, is_valid: bool, is_initialized: bool, is_authenticated: bool, notice_text: Text, poll_instant: Instant, shared: Option, password_entered: bool, dirty_flag: Option>, album_art: Option, album_art_draw_transform: Option, filename_text: Text, filename_string_cache: String, filename_transform: Transform, artist_text: Text, artist_string_cache: String, artist_transform: Transform, title_text: Text, title_string_cache: String, title_transform: Transform, timer_text: Text, timer_transform: Transform, timer_x: f32, timer_y: f32, timer: f64, length: f64, text_bg_mesh: Option, hide_text: bool, tried_album_art_in_dir: bool, mpd_play_state: MPDPlayState, loaded_fonts: Vec<(PathBuf, Font)>, } impl MPDDisplay { pub fn new(_ctx: &mut Context, opts: Opt) -> Self { Self { opts, mpd_handler: Err(String::from("Uninitialized")), is_valid: true, is_initialized: false, is_authenticated: false, notice_text: Text::new(""), poll_instant: Instant::now() - POLL_TIME, shared: None, password_entered: false, dirty_flag: None, album_art: None, album_art_draw_transform: None, filename_text: Text::new(""), filename_transform: Transform::default(), artist_text: Text::new(""), artist_transform: Transform::default(), title_text: Text::new(""), title_transform: Transform::default(), timer_text: Text::new("0"), timer_transform: Transform::default(), timer_x: INIT_FONT_SIZE_X, timer_y: INIT_FONT_SIZE_Y, timer: 0.0, length: 0.0, text_bg_mesh: None, hide_text: false, tried_album_art_in_dir: false, mpd_play_state: MPDPlayState::Playing, loaded_fonts: Vec::new(), filename_string_cache: String::new(), artist_string_cache: String::new(), title_string_cache: String::new(), } } fn init_mpd_handler(&mut self) { self.mpd_handler = MPDHandler::new( self.opts.host, self.opts.port, self.opts.password.clone().map_or(String::new(), |s| s), self.opts.log_level, ); if self.mpd_handler.is_ok() { self.is_initialized = true; loop { self.dirty_flag = self.mpd_handler.as_ref().unwrap().get_dirty_flag().ok(); if self.dirty_flag.is_some() { break; } else { thread::sleep(POLL_TIME); } } log( "Successfully initialized MPDHandler", debug_log::LogState::Debug, self.opts.log_level, ); } else { self.is_valid = false; log( "Failed to initialize MPDHandler", debug_log::LogState::Debug, self.opts.log_level, ); } } fn get_album_art_transform(&mut self, ctx: &mut Context, fill_scaled: bool) { if fill_scaled { if let Some(image) = &self.album_art { let screen_coords: Rect = graphics::screen_coordinates(ctx); let art_rect: Rect = image.dimensions(); // try to fit to width first let mut x_scale = screen_coords.w / art_rect.w; let mut y_scale = x_scale; let mut new_width = art_rect.w * x_scale; let mut new_height = art_rect.h * y_scale; if new_height > screen_coords.h.abs() { // fit to height instead y_scale = screen_coords.h.abs() / art_rect.h; x_scale = y_scale; new_width = art_rect.w * x_scale; new_height = art_rect.h * y_scale; } let offset_x: f32 = (screen_coords.w.abs() - new_width) / 2.0f32; let offset_y: f32 = (screen_coords.h.abs() - new_height) / 2.0f32; self.album_art_draw_transform = Some(Transform::Values { dest: [offset_x, offset_y].into(), rotation: 0.0f32, scale: [x_scale, y_scale].into(), offset: [0.0f32, 0.0f32].into(), }); } else { self.album_art_draw_transform = None; } } else if let Some(image) = &self.album_art { let screen_coords: Rect = graphics::screen_coordinates(ctx); let art_rect: Rect = image.dimensions(); let offset_x: f32 = (screen_coords.w.abs() - art_rect.w.abs()) / 2.0f32; let offset_y: f32 = (screen_coords.h.abs() - art_rect.h.abs()) / 2.0f32; self.album_art_draw_transform = Some(Transform::Values { dest: [offset_x, offset_y].into(), rotation: 0.0f32, scale: [1.0f32, 1.0f32].into(), offset: [0.0f32, 0.0f32].into(), }); } else { self.album_art_draw_transform = None; } } fn get_image_from_data(&mut self, ctx: &mut Context) -> Result<(), String> { let mut read_guard_opt: Option> = self .mpd_handler .as_ref() .unwrap() .get_state_read_guard() .ok(); if read_guard_opt.is_none() { return Err(String::from("Failed to get read_guard of MPDHandlerState")); } else if !read_guard_opt.as_ref().unwrap().is_art_data_ready() { return Err(String::from("MPDHandlerState does not have album art data")); } let image_ref = read_guard_opt.as_ref().unwrap().get_art_data(); let mut image_format: image::ImageFormat = image::ImageFormat::Png; log( format!( "Got image_format type {}", read_guard_opt.as_ref().unwrap().get_art_type() ), debug_log::LogState::Debug, self.opts.log_level, ); let mut is_unknown_format: bool = false; match read_guard_opt.as_ref().unwrap().get_art_type().as_str() { "image/png" => image_format = image::ImageFormat::Png, "image/jpg" | "image/jpeg" | "JPG" => image_format = image::ImageFormat::Jpeg, "image/gif" => image_format = image::ImageFormat::Gif, _ => is_unknown_format = true, } let try_second_art_fetch_method = |tried_in_dir: &mut bool, album_art: &mut Option, read_guard_opt: &mut Option< RwLockReadGuard<'_, MPDHandlerState>, >, mpd_handler: &Result| -> Result<(), String> { *tried_in_dir = true; album_art.take(); // Drop the "read_guard" so that the "force_try_other_album_art()" // can get a "write_guard" read_guard_opt.take(); mpd_handler .as_ref() .unwrap() .force_try_other_album_art() .map_err(|_| String::from("Failed to force try other album art fetching method"))?; Err("Got unknown format album art image".into()) }; if is_unknown_format && !self.tried_album_art_in_dir { return try_second_art_fetch_method( &mut self.tried_album_art_in_dir, &mut self.album_art, &mut read_guard_opt, &self.mpd_handler, ); } let img_result = ImageReader::with_format(Cursor::new(&image_ref), image_format) .decode() .map_err(|e| format!("Error: Failed to decode album art image: {}", e)); if img_result.is_err() && !self.tried_album_art_in_dir { return try_second_art_fetch_method( &mut self.tried_album_art_in_dir, &mut self.album_art, &mut read_guard_opt, &self.mpd_handler, ); } let img = img_result?; let rgba8 = img.to_rgba8(); let ggez_img = Image::from_rgba8( ctx, rgba8.width() as u16, rgba8.height() as u16, rgba8.as_raw(), ) .map_err(|e| format!("Error: Failed to load album art image in ggez Image: {}", e))?; self.album_art = Some(ggez_img); Ok(()) } fn refresh_text_transforms(&mut self, ctx: &mut Context) -> GameResult<()> { let screen_coords: Rect = graphics::screen_coordinates(ctx); let text_height_limit = TEXT_HEIGHT_SCALE * screen_coords.h.abs(); let artist_height_limit = ARTIST_HEIGHT_SCALE * screen_coords.h.abs(); let timer_height = TIMER_HEIGHT_SCALE * screen_coords.h.abs(); let mut offset_y: f32 = screen_coords.h; let mut filename_y: f32 = 0.0; let mut artist_y: f32 = 0.0; let mut title_y: f32 = 0.0; let mut timer_y: f32 = 0.0; let set_transform = |text: &mut Text, transform: &mut Transform, offset_y: &mut f32, y: &mut f32, is_string: bool, is_artist: bool, timer_x: &mut f32, timer_y: &mut f32| { let mut current_x = INIT_FONT_SIZE_X; let mut current_y = INIT_FONT_SIZE_Y; let mut width: f32; let mut height: f32 = 0.0; let mut iteration_count: u8 = 0; loop { iteration_count += 1; if iteration_count > 8 { break; } for fragment in text.fragments_mut() { fragment.scale = Some(PxScale { x: current_x, y: current_y, }); } width = text.width(ctx); height = text.height(ctx); if is_string { if screen_coords.w < width || height >= (if is_artist { artist_height_limit } else { text_height_limit }) { current_x *= DECREASE_AMT; current_y *= DECREASE_AMT; continue; } else if screen_coords.w * MIN_WIDTH_RATIO > width { current_x *= INCREASE_AMT; current_y *= INCREASE_AMT; continue; } else { break; } } else { let diff_scale_y = current_y / height * timer_height; let current_x = current_x * diff_scale_y / current_y; for fragment in text.fragments_mut() { fragment.scale = Some(PxScale { x: current_x, y: diff_scale_y, }); } *timer_x = current_x; *timer_y = diff_scale_y; // width = text.width(ctx); // not really used after this height = text.height(ctx); break; } } *y = *offset_y - height; *transform = Transform::Values { dest: [TEXT_X_OFFSET, *offset_y - height].into(), rotation: 0.0, scale: [1.0, 1.0].into(), offset: [0.0, 0.0].into(), }; *offset_y -= height + TEXT_OFFSET_Y_SPACING; }; if !self.filename_text.contents().is_empty() && !self.opts.disable_show_filename { set_transform( &mut self.filename_text, &mut self.filename_transform, &mut offset_y, &mut filename_y, true, false, &mut self.timer_x, &mut self.timer_y, ); } else { log( "filename text is empty", debug_log::LogState::Warning, self.opts.log_level, ); } if !self.artist_text.contents().is_empty() && !self.opts.disable_show_artist { set_transform( &mut self.artist_text, &mut self.artist_transform, &mut offset_y, &mut artist_y, true, true, &mut self.timer_x, &mut self.timer_y, ); } else { log( "artist text is empty", debug_log::LogState::Warning, self.opts.log_level, ); } if !self.title_text.contents().is_empty() && !self.opts.disable_show_title { set_transform( &mut self.title_text, &mut self.title_transform, &mut offset_y, &mut title_y, true, false, &mut self.timer_x, &mut self.timer_y, ); } else { log( "title text is empty", debug_log::LogState::Warning, self.opts.log_level, ); } set_transform( &mut self.timer_text, &mut self.timer_transform, &mut offset_y, &mut timer_y, false, false, &mut self.timer_x, &mut self.timer_y, ); let filename_dimensions = self.filename_text.dimensions(ctx); let artist_dimensions = self.artist_text.dimensions(ctx); let title_dimensions = self.title_text.dimensions(ctx); let timer_dimensions = self.timer_text.dimensions(ctx); let mut mesh_builder: MeshBuilder = MeshBuilder::new(); if !self.opts.disable_show_filename { mesh_builder.rectangle( DrawMode::fill(), Rect { x: TEXT_X_OFFSET, y: filename_y, w: filename_dimensions.w, h: filename_dimensions.h, }, Color::from_rgba(0, 0, 0, 160), )?; } if !self.opts.disable_show_artist { mesh_builder.rectangle( DrawMode::fill(), Rect { x: TEXT_X_OFFSET, y: artist_y, w: artist_dimensions.w, h: artist_dimensions.h, }, Color::from_rgba(0, 0, 0, 160), )?; } if !self.opts.disable_show_title { mesh_builder.rectangle( DrawMode::fill(), Rect { x: TEXT_X_OFFSET, y: title_y, w: title_dimensions.w, h: title_dimensions.h, }, Color::from_rgba(0, 0, 0, 160), )?; } let mesh: Mesh = mesh_builder .rectangle( DrawMode::fill(), Rect { x: TEXT_X_OFFSET, y: timer_y, w: timer_dimensions.w, h: timer_dimensions.h, }, Color::from_rgba(0, 0, 0, 160), )? .build(ctx)?; self.text_bg_mesh = Some(mesh); Ok(()) } } impl EventHandler for MPDDisplay { fn update(&mut self, ctx: &mut ggez::Context) -> Result<(), GameError> { if !self.is_valid { if let Err(mpd_handler_error) = &self.mpd_handler { return Err(GameError::EventLoopError(format!( "Failed to initialize MPDHandler: {}", mpd_handler_error ))); } else { return Err(GameError::EventLoopError( "Failed to initialize MPDHandler".into(), )); } } if !self.is_initialized { if self.opts.enable_prompt_password { if self.notice_text.contents().is_empty() { self.notice_text = Text::new(TextFragment::new("password: ")); } else if self.password_entered { self.init_mpd_handler(); } } else { self.init_mpd_handler(); } } else if self.password_entered { 'check_state: loop { let result = self.mpd_handler.as_ref().unwrap().is_authenticated(); if let Ok(true) = result { self.is_authenticated = true; break; } else if let Err(()) = result { continue; } else { loop { let check_fail_result = self.mpd_handler.as_ref().unwrap().failed_to_authenticate(); if let Ok(true) = check_fail_result { { let mpd_handler = self.mpd_handler.clone().unwrap(); loop { let stop_thread_result = mpd_handler.stop_thread(); if stop_thread_result.is_ok() { break; } } } self.notice_text = Text::new(TextFragment::new("password: ")); self.opts.password = Some(String::new()); self.password_entered = false; self.is_initialized = false; break 'check_state; } else if let Err(()) = check_fail_result { continue; } else { break 'check_state; } } } } } if self.is_valid && self.is_initialized && self.poll_instant.elapsed() > POLL_TIME { self.poll_instant = Instant::now(); if self.dirty_flag.is_some() && self .dirty_flag .as_ref() .unwrap() .swap(false, Ordering::Relaxed) { log( "dirty_flag cleared, acquiring shared data...", debug_log::LogState::Debug, self.opts.log_level, ); self.shared = self .mpd_handler .as_ref() .unwrap() .get_mpd_handler_shared_state() .ok(); if let Some(shared) = &self.shared { if self.notice_text.contents() != shared.error_text { self.notice_text = Text::new(TextFragment::new(shared.error_text.clone())); } if shared.mpd_play_state != MPDPlayState::Playing { if shared.mpd_play_state == MPDPlayState::Stopped { self.title_text = Text::new(""); self.artist_text = Text::new(""); self.filename_text = Text::new(""); self.timer = 0.0; self.length = 0.0; self.album_art = None; } self.mpd_play_state = shared.mpd_play_state; } else { self.mpd_play_state = MPDPlayState::Playing; if !shared.title.is_empty() { if shared.title != self.title_string_cache { self.title_string_cache = shared.title.clone(); self.title_text = string_to_text( shared.title.clone(), &mut self.loaded_fonts, ctx, ); } } else { self.dirty_flag .as_ref() .unwrap() .store(true, Ordering::Relaxed); } if !shared.artist.is_empty() { if shared.artist != self.artist_string_cache { self.artist_string_cache = shared.artist.clone(); self.artist_text = string_to_text( shared.artist.clone(), &mut self.loaded_fonts, ctx, ); } } else { self.dirty_flag .as_ref() .unwrap() .store(true, Ordering::Relaxed); } if !shared.filename.is_empty() { if shared.filename != self.filename_string_cache { self.filename_string_cache = shared.filename.clone(); if self.filename_text.contents() != shared.filename { self.album_art = None; self.tried_album_art_in_dir = false; } self.filename_text = string_to_text( shared.filename.clone(), &mut self.loaded_fonts, ctx, ); } } else { self.dirty_flag .as_ref() .unwrap() .store(true, Ordering::Relaxed); } self.timer = shared.pos; self.length = shared.length; self.refresh_text_transforms(ctx)?; } } else { log( "Failed to acquire read lock for getting shared data", debug_log::LogState::Debug, self.opts.log_level, ); } if self.album_art.is_none() { let result = self.get_image_from_data(ctx); if let Err(e) = result { log(e, debug_log::LogState::Warning, self.opts.log_level); self.album_art = None; self.album_art_draw_transform = None; } else { self.get_album_art_transform(ctx, !self.opts.do_not_fill_scale_album_art); } } } } let delta = timer::delta(ctx); self.timer += delta.as_secs_f64(); let timer_diff = seconds_to_time(self.length - self.timer); self.timer_text = Text::new(timer_diff); self.timer_text.set_font( Font::default(), PxScale { x: self.timer_x, y: self.timer_y, }, ); Ok(()) } fn draw(&mut self, ctx: &mut ggez::Context) -> Result<(), GameError> { graphics::clear(ctx, Color::BLACK); if self.mpd_play_state != MPDPlayState::Stopped && self.album_art.is_some() && self.album_art_draw_transform.is_some() { self.album_art.as_ref().unwrap().draw( ctx, DrawParam { trans: self.album_art_draw_transform.unwrap(), ..Default::default() }, )?; } if !self.hide_text { self.notice_text.draw(ctx, DrawParam::default())?; if self.mpd_play_state != MPDPlayState::Stopped && self.is_valid && self.is_initialized { if let Some(mesh) = &self.text_bg_mesh { mesh.draw(ctx, DrawParam::default())?; } if !self.opts.disable_show_filename { self.filename_text.draw( ctx, DrawParam { trans: self.filename_transform, ..Default::default() }, )?; } if !self.opts.disable_show_artist { self.artist_text.draw( ctx, DrawParam { trans: self.artist_transform, ..Default::default() }, )?; } if !self.opts.disable_show_title { self.title_text.draw( ctx, DrawParam { trans: self.title_transform, ..Default::default() }, )?; } if self.mpd_play_state == MPDPlayState::Playing { self.timer_text.draw( ctx, DrawParam { trans: self.timer_transform, ..Default::default() }, )?; } } } graphics::present(ctx) } fn text_input_event(&mut self, _ctx: &mut Context, character: char) { if !self.is_initialized && self.opts.enable_prompt_password && !character.is_control() { if self.opts.password.is_none() { let s = String::from(character); self.opts.password = Some(s); self.notice_text.add('*'); } else { self.opts.password.as_mut().unwrap().push(character); self.notice_text.add('*'); } } } fn key_down_event( &mut self, _ctx: &mut Context, keycode: event::KeyCode, _keymods: event::KeyMods, _repeat: bool, ) { if !self.is_initialized && self.opts.enable_prompt_password { if keycode == event::KeyCode::Back { let s: String = self.notice_text.contents(); if s.ends_with('*') { self.notice_text = Text::new(TextFragment::new(s[0..(s.len() - 1)].to_owned())); } if let Some(input_p) = &mut self.opts.password { input_p.pop(); } } else if keycode == event::KeyCode::Return { self.password_entered = true; } } else if keycode == event::KeyCode::H { self.hide_text = true; } } fn key_up_event( &mut self, _ctx: &mut Context, keycode: event::KeyCode, _keymods: event::KeyMods, ) { if keycode == event::KeyCode::H { self.hide_text = false; } } fn resize_event(&mut self, ctx: &mut Context, _width: f32, _height: f32) { self.get_album_art_transform(ctx, !self.opts.do_not_fill_scale_album_art); self.refresh_text_transforms(ctx) .expect("Failed to set text transforms"); } }