diff --git a/komorebi-bar/src/config.rs b/komorebi-bar/src/config.rs index 21d67cf8..8216441b 100644 --- a/komorebi-bar/src/config.rs +++ b/komorebi-bar/src/config.rs @@ -621,6 +621,26 @@ extend_enum!( AllIconsAndTextOnSelected, }); +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +/// Media widget display format +pub enum MediaDisplayFormat { + /// Show only the media info icon + Icon, + /// Show only the media info text (artist - title) + Text, + /// Show both icon and text + IconAndText, + /// Show only the control buttons (previous, play/pause, next) + ControlsOnly, + /// Show icon with control buttons + IconAndControls, + /// Show text with control buttons + TextAndControls, + /// Show icon, text, and control buttons + Full, +} + #[cfg(test)] mod tests { use serde::Deserialize; diff --git a/komorebi-bar/src/widgets/media.rs b/komorebi-bar/src/widgets/media.rs index 2f218c61..e0155fe0 100644 --- a/komorebi-bar/src/widgets/media.rs +++ b/komorebi-bar/src/widgets/media.rs @@ -1,4 +1,6 @@ use crate::MAX_LABEL_WIDTH; +use crate::bar::Alignment; +use crate::config::MediaDisplayFormat; use crate::render::RenderConfig; use crate::selected_frame::SelectableFrame; use crate::ui::CustomUi; @@ -14,6 +16,7 @@ use serde::Deserialize; use serde::Serialize; use std::sync::atomic::Ordering; use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager; +use windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -21,24 +24,31 @@ use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager; pub struct MediaConfig { /// Enable the Media widget pub enable: bool, + /// Display format of the media widget (defaults to IconAndText) + pub display: Option, } impl From for Media { fn from(value: MediaConfig) -> Self { - Self::new(value.enable) + Self::new( + value.enable, + value.display.unwrap_or(MediaDisplayFormat::IconAndText), + ) } } #[derive(Clone, Debug)] pub struct Media { pub enable: bool, + pub display: MediaDisplayFormat, pub session_manager: GlobalSystemMediaTransportControlsSessionManager, } impl Media { - pub fn new(enable: bool) -> Self { + pub fn new(enable: bool, display: MediaDisplayFormat) -> Self { Self { enable, + display, session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync() .unwrap() .join() @@ -54,6 +64,58 @@ impl Media { } } + pub fn previous(&self) { + if let Ok(session) = self.session_manager.GetCurrentSession() + && let Ok(op) = session.TrySkipPreviousAsync() + { + op.join().unwrap_or_default(); + } + } + + pub fn next(&self) { + if let Ok(session) = self.session_manager.GetCurrentSession() + && let Ok(op) = session.TrySkipNextAsync() + { + op.join().unwrap_or_default(); + } + } + + fn is_playing(&self) -> bool { + if let Ok(session) = self.session_manager.GetCurrentSession() + && let Ok(info) = session.GetPlaybackInfo() + && let Ok(status) = info.PlaybackStatus() + { + return status == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing; + } + false + } + + fn is_previous_enabled(&self) -> bool { + if let Ok(session) = self.session_manager.GetCurrentSession() + && let Ok(info) = session.GetPlaybackInfo() + && let Ok(controls) = info.Controls() + && let Ok(enabled) = controls.IsPreviousEnabled() + { + return enabled; + } + false + } + + fn is_next_enabled(&self) -> bool { + if let Ok(session) = self.session_manager.GetCurrentSession() + && let Ok(info) = session.GetPlaybackInfo() + && let Ok(controls) = info.Controls() + && let Ok(enabled) = controls.IsNextEnabled() + { + return enabled; + } + false + } + + fn has_session(&self) -> bool { + self.session_manager.GetCurrentSession().is_ok() + } + fn output(&mut self) -> String { if let Ok(session) = self.session_manager.GetCurrentSession() && let Ok(operation) = session.TryGetMediaPropertiesAsync() @@ -78,28 +140,96 @@ impl Media { impl BarWidget for Media { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { + // Don't render if there's no active media session + if !self.has_session() { + return; + } + let output = self.output(); - if !output.is_empty() { - let mut layout_job = LayoutJob::simple( + + let show_icon = matches!( + self.display, + MediaDisplayFormat::Icon + | MediaDisplayFormat::IconAndText + | MediaDisplayFormat::IconAndControls + | MediaDisplayFormat::Full + ); + let show_text = matches!( + self.display, + MediaDisplayFormat::Text + | MediaDisplayFormat::IconAndText + | MediaDisplayFormat::TextAndControls + | MediaDisplayFormat::Full + ); + let show_controls = matches!( + self.display, + MediaDisplayFormat::ControlsOnly + | MediaDisplayFormat::IconAndControls + | MediaDisplayFormat::TextAndControls + | MediaDisplayFormat::Full + ); + + // Don't render if there's no media info and we're not showing controls-only + if output.is_empty() && !show_controls { + return; + } + + let icon_font_id = config.icon_font_id.clone(); + let text_font_id = config.text_font_id.clone(); + let icon_color = ctx.style().visuals.selection.stroke.color; + let text_color = ctx.style().visuals.text_color(); + + let mut layout_job = LayoutJob::default(); + + if show_icon { + layout_job = LayoutJob::simple( egui_phosphor::regular::HEADPHONES.to_string(), - config.icon_font_id.clone(), - ctx.style().visuals.selection.stroke.color, + icon_font_id.clone(), + icon_color, 100.0, ); + } + if show_text { layout_job.append( &output, - 10.0, + if show_icon { 10.0 } else { 0.0 }, TextFormat { - font_id: config.text_font_id.clone(), - color: ctx.style().visuals.text_color(), + font_id: text_font_id, + color: text_color, valign: Align::Center, ..Default::default() }, ); + } - config.apply_on_widget(false, ui, |ui| { - if SelectableFrame::new(false) + let is_playing = self.is_playing(); + let is_previous_enabled = self.is_previous_enabled(); + let is_next_enabled = self.is_next_enabled(); + let disabled_color = text_color.gamma_multiply(0.5); + let is_reversed = matches!(config.alignment, Some(Alignment::Right)); + + let prev_color = if is_previous_enabled { + text_color + } else { + disabled_color + }; + + let next_color = if is_next_enabled { + text_color + } else { + disabled_color + }; + + let play_pause_icon = if is_playing { + egui_phosphor::regular::PAUSE + } else { + egui_phosphor::regular::PLAY + }; + + let show_label = |ui: &mut Ui| { + if (show_icon || show_text) + && SelectableFrame::new(false) .show(ui, |ui| { let available_height = ui.available_height(); let mut custom_ui = CustomUi(ui); @@ -109,15 +239,95 @@ impl BarWidget for Media { MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, available_height, ), - Label::new(layout_job).selectable(false).truncate(), + Label::new(layout_job.clone()).selectable(false).truncate(), ) }) + .on_hover_text(&output) .clicked() - { - self.toggle(); + { + self.toggle(); + } + }; + + let show_previous = |ui: &mut Ui| { + if SelectableFrame::new(false) + .show(ui, |ui| { + ui.add( + Label::new(LayoutJob::simple( + egui_phosphor::regular::SKIP_BACK.to_string(), + icon_font_id.clone(), + prev_color, + 100.0, + )) + .selectable(false), + ) + }) + .clicked() + && is_previous_enabled + { + self.previous(); + } + }; + + let show_play_pause = |ui: &mut Ui| { + if SelectableFrame::new(false) + .show(ui, |ui| { + ui.add( + Label::new(LayoutJob::simple( + play_pause_icon.to_string(), + icon_font_id.clone(), + text_color, + 100.0, + )) + .selectable(false), + ) + }) + .on_hover_text(&output) + .clicked() + { + self.toggle(); + } + }; + + let show_next = |ui: &mut Ui| { + if SelectableFrame::new(false) + .show(ui, |ui| { + ui.add( + Label::new(LayoutJob::simple( + egui_phosphor::regular::SKIP_FORWARD.to_string(), + icon_font_id.clone(), + next_color, + 100.0, + )) + .selectable(false), + ) + }) + .clicked() + && is_next_enabled + { + self.next(); + } + }; + + config.apply_on_widget(false, ui, |ui| { + if is_reversed { + // Right panel renders right-to-left, so reverse order + if show_controls { + show_next(ui); + show_play_pause(ui); + show_previous(ui); } - }); - } + show_label(ui); + } else { + // Left/center panel renders left-to-right, normal order + show_label(ui); + if show_controls { + show_previous(ui); + show_play_pause(ui); + show_next(ui); + } + } + }); } } } diff --git a/schema.bar.json b/schema.bar.json index b7b73761..6c7396a7 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -3863,6 +3863,17 @@ "description": "Media widget configuration", "type": "object", "properties": { + "display": { + "description": "Display format of the media widget (defaults to IconAndText)", + "anyOf": [ + { + "$ref": "#/$defs/MediaDisplayFormat" + }, + { + "type": "null" + } + ] + }, "enable": { "description": "Enable the Media widget", "type": "boolean" @@ -3872,6 +3883,46 @@ "enable" ] }, + "MediaDisplayFormat": { + "description": "Media widget display format", + "oneOf": [ + { + "description": "Show only the media info icon", + "type": "string", + "const": "Icon" + }, + { + "description": "Show only the media info text (artist - title)", + "type": "string", + "const": "Text" + }, + { + "description": "Show both icon and text", + "type": "string", + "const": "IconAndText" + }, + { + "description": "Show only the control buttons (previous, play/pause, next)", + "type": "string", + "const": "ControlsOnly" + }, + { + "description": "Show icon with control buttons", + "type": "string", + "const": "IconAndControls" + }, + { + "description": "Show text with control buttons", + "type": "string", + "const": "TextAndControls" + }, + { + "description": "Show icon, text, and control buttons", + "type": "string", + "const": "Full" + } + ] + }, "MemoryConfig": { "description": "Memory widget configuration", "type": "object",