mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-03-28 04:11:29 +01:00
feat(bar): enhancing the media widget
The Media widget has been enhanced with a new MediaDisplayFormat configuration option that controls how the widget is displayed. It supports seven formats: Icon, Text, IconAndText (the default), ControlsOnly, IconAndControls, TextAndControls, and Full. The widget now detects whether the Previous and Next buttons are actually available for the current media session using the Windows Media Control API. When a button is not available, it appears dimmed at 50 percent opacity and clicking it has no effect. Tooltips were added to improve usability. Hovering over the media info label shows the full artist and title text, which is helpful when the text is truncated. The Play/Pause button also shows the media info on hover. The rendering logic was refactored to properly handle right-aligned widgets. When the Media widget is placed in right_widgets, the UI renders items from right to left, so the code now renders elements in reverse order to ensure the visual appearance remains consistent regardless of which panel the widget is placed in.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<MediaDisplayFormat>,
|
||||
}
|
||||
|
||||
impl From<MediaConfig> 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user