Add panel-wide styles and the Morph hub (#26)
Panel appearance now applies to the whole panel, with seven styles: Floating, Capsules, Notch, Edge to edge, Fused islands, Adaptive and Morph. - Share composition, geometry, and motion through the renderer-independent `ferese-panel` crate, including native materials, input regions, and Settings previews. - Add the Morph hub with media, calendar, notifications, controls, workspace previews, hover feedback, and attached mode. Keep recording and low-battery status visible and respect window clearance. - Cache workspace preview geometry, draw thumbnails directly, centralize Morph transitions, and keep rendering settings local to each shell instance. - Put Record Screen in the Morph header and show the display control only when an external monitor is connected. Update panel documentation, the desktop screenshot, and native check style names. Local validation included 660 compositor tests, 191 shell tests, isolated preview-cache checks, GPU Morph captures at 1×/1.25×/1.5×, formatting, and Clippy with warnings denied. The final header change passed all 23 Morph tests. Release workflow jobs are skipped for this PR.
This commit is contained in:
1 parent
42c3b290dd
commit
e22f0ebef9
131 files changed
+12562
-1634
No files matched your search
@@ -6,7 +6,7 @@ use super::bar::audio_icon;
|
||||
use super::controls::{control_card, shell_switch, slider_row};
|
||||
use super::{MenuRows, MenuStyle};
|
||||
use crate::status::Action;
|
||||
use crate::{FereseShell, Message, color_with_opacity, text};
|
||||
use crate::{FereseShell, Message, color_with_opacity};
|
||||
|
||||
pub(super) fn view<'a>(
|
||||
shell: &'a FereseShell,
|
||||
@@ -21,14 +21,23 @@ pub(super) fn view<'a>(
|
||||
opacity: p,
|
||||
} = style;
|
||||
if let Some(a) = &shell.status.audio {
|
||||
let mut volume_heading = row![text("Volume").size(13).width(Length::Fill)].align_y(Alignment::Center);
|
||||
let mut volume_heading =
|
||||
row![shell.render.text("Volume").size(13).width(Length::Fill)].align_y(Alignment::Center);
|
||||
if combined {
|
||||
volume_heading = volume_heading.push(shell_switch("Mute", a.muted, Action::Mute(!a.muted), theme, p));
|
||||
volume_heading = volume_heading.push(shell_switch(
|
||||
shell.render,
|
||||
"Mute",
|
||||
a.muted,
|
||||
Action::Mute(!a.muted),
|
||||
theme,
|
||||
p,
|
||||
));
|
||||
}
|
||||
let audio = column![
|
||||
volume_heading,
|
||||
text(&a.output).size(12).class(theme::Text::Color(muted)),
|
||||
shell.render.text(&a.output).size(12).class(theme::Text::Color(muted)),
|
||||
slider_row(
|
||||
shell.render,
|
||||
audio_icon(a.volume, a.muted),
|
||||
a.volume,
|
||||
false,
|
||||
@@ -39,7 +48,7 @@ pub(super) fn view<'a>(
|
||||
]
|
||||
.spacing(4);
|
||||
let audio: Element<'_, cosmic::Action<Message>> = if combined {
|
||||
control_card(audio.into(), primary, p)
|
||||
control_card(shell.render, audio.into(), primary, p)
|
||||
} else {
|
||||
audio.into()
|
||||
};
|
||||
@@ -55,7 +64,14 @@ pub(super) fn heading<'a>(
|
||||
) -> super::MenuHeading<'a> {
|
||||
let MenuStyle { theme, opacity: p, .. } = style;
|
||||
if let Some(audio) = &shell.status.audio {
|
||||
heading = heading.push(shell_switch("Mute", audio.muted, Action::Mute(!audio.muted), theme, p));
|
||||
heading = heading.push(shell_switch(
|
||||
shell.render,
|
||||
"Mute",
|
||||
audio.muted,
|
||||
Action::Mute(!audio.muted),
|
||||
theme,
|
||||
p,
|
||||
));
|
||||
}
|
||||
heading
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use cosmic::{Element, theme};
|
||||
|
||||
use super::Menu;
|
||||
use crate::status::Snapshot;
|
||||
use crate::{BarMetrics, FereseShell, Message, accented_icon, bar_content, color, motion, recording, text};
|
||||
use crate::{BarMetrics, FereseShell, Message, accented_icon, bar_content, color, motion, recording};
|
||||
|
||||
impl FereseShell {
|
||||
pub(crate) fn view_media_item(
|
||||
@@ -21,7 +21,7 @@ impl FereseShell {
|
||||
super::media::bar(
|
||||
self,
|
||||
theme,
|
||||
BarMetrics::from(self.config.panels[0].geometry),
|
||||
BarMetrics::from(self.config.panels[0].resolved_geometry()),
|
||||
representation,
|
||||
selected,
|
||||
width,
|
||||
@@ -36,7 +36,7 @@ impl FereseShell {
|
||||
selected: bool,
|
||||
) -> Element<'_, cosmic::Action<Message>> {
|
||||
let theme = self.config.theme.for_bar();
|
||||
let metrics = BarMetrics::from(self.config.panels[0].geometry);
|
||||
let metrics = BarMetrics::from(self.config.panels[0].resolved_geometry());
|
||||
let (source, enabled) = if kind == Menu::Recording {
|
||||
let source: &'static [u8] = match self.recorder.state {
|
||||
recording::State::Selecting => ferese_theme::icons::RECORD_CANCEL,
|
||||
@@ -70,7 +70,8 @@ impl FereseShell {
|
||||
&& let Some(elapsed) = self.recorder.elapsed()
|
||||
{
|
||||
content = content.push(
|
||||
text(elapsed)
|
||||
self.render
|
||||
.text(elapsed)
|
||||
.size(metrics.text_size)
|
||||
.class(theme::Text::Color(foreground)),
|
||||
);
|
||||
@@ -81,7 +82,8 @@ impl FereseShell {
|
||||
&& let Some(battery) = &self.status.battery
|
||||
{
|
||||
content = content.push(
|
||||
text(format!("{}%", battery.percent))
|
||||
self.render
|
||||
.text(format!("{}%", battery.percent))
|
||||
.size(metrics.text_size)
|
||||
.class(theme::Text::Color(foreground)),
|
||||
);
|
||||
@@ -94,14 +96,14 @@ impl FereseShell {
|
||||
.is_some_and(|n| n.count > 0 && !n.dnd)
|
||||
{
|
||||
content = content.push(
|
||||
container(text(""))
|
||||
container(self.render.text(""))
|
||||
.width(4)
|
||||
.height(4)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(foreground)),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(2.0).into(),
|
||||
radius: self.render.radius(2.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -138,7 +140,7 @@ impl FereseShell {
|
||||
},
|
||||
))
|
||||
});
|
||||
motion::button(control, foreground, selected, 1.0)
|
||||
motion::button(self.render, control, foreground, selected, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +163,7 @@ fn status_label(kind: Menu, s: &Snapshot) -> String {
|
||||
Some(n) => format!("Notifications: {} unread", n.count),
|
||||
_ => "Notifications: unavailable".into(),
|
||||
},
|
||||
Menu::Workspaces => "Workspaces".into(),
|
||||
Menu::Calendar => "Calendar".into(),
|
||||
Menu::Overflow => "More panel items".into(),
|
||||
Menu::Recording => "Screen recording".into(),
|
||||
@@ -239,6 +242,7 @@ pub(super) fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
||||
(ferese_theme::icons::NOTIFICATIONS, true)
|
||||
}
|
||||
}
|
||||
Menu::Workspaces => (ferese_theme::icons::OVERVIEW, true),
|
||||
Menu::System => (ferese_theme::icons::CONTROL_CENTER, true),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::bar::status_icon;
|
||||
use super::controls::{control_card, level_meter, slider_row};
|
||||
use super::{Menu, MenuRows, MenuStyle};
|
||||
use crate::status::Action;
|
||||
use crate::{FereseShell, Message, ShellTheme, accented_icon, bar_icon, color_with_opacity, motion, shell_font, text};
|
||||
use crate::{FereseShell, Message, ShellTheme, accented_icon, bar_icon, color_with_opacity, motion};
|
||||
|
||||
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
||||
let MenuStyle {
|
||||
@@ -27,7 +27,11 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
rows = rows.push(
|
||||
container(
|
||||
row![
|
||||
text(format!("{}%", b.percent)).size(24).width(Length::Fill),
|
||||
shell
|
||||
.render
|
||||
.text(format!("{}%", b.percent))
|
||||
.size(24)
|
||||
.width(Length::Fill),
|
||||
accented_icon(
|
||||
status_icon(Menu::Battery, &shell.status).0,
|
||||
24,
|
||||
@@ -44,19 +48,25 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
..Default::default()
|
||||
})),
|
||||
);
|
||||
rows = rows.push(level_meter(b.percent, battery_color, p));
|
||||
rows = rows.push(level_meter(shell.render, b.percent, battery_color, p));
|
||||
rows = rows.push(
|
||||
text(match b.status.as_str() {
|
||||
"Charging" => "Connected to power",
|
||||
"Discharging" => "Running on battery power",
|
||||
"Full" => "Battery fully charged",
|
||||
_ => "Battery status reported by the system",
|
||||
})
|
||||
.size(12)
|
||||
.class(theme::Text::Color(muted)),
|
||||
shell
|
||||
.render
|
||||
.text(match b.status.as_str() {
|
||||
"Charging" => "Connected to power",
|
||||
"Discharging" => "Running on battery power",
|
||||
"Full" => "Battery fully charged",
|
||||
_ => "Battery status reported by the system",
|
||||
})
|
||||
.size(12)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
rows = rows.push(ferese_theme::menus::section_label("Power mode", shell_font(), muted));
|
||||
rows = rows.push(ferese_theme::menus::section_label(
|
||||
"Power mode",
|
||||
shell.render.font,
|
||||
muted,
|
||||
));
|
||||
if let Some(profiles) = &shell.status.power_profiles {
|
||||
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (index, (label, icon, profile)) in [
|
||||
@@ -68,13 +78,23 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
.enumerate()
|
||||
{
|
||||
if profiles.available[index] {
|
||||
modes = modes.push(power_tile(label, icon, profile, profiles.active == profile, theme, p));
|
||||
modes = modes.push(power_tile(
|
||||
shell.render,
|
||||
label,
|
||||
icon,
|
||||
profile,
|
||||
profiles.active == profile,
|
||||
theme,
|
||||
p,
|
||||
));
|
||||
}
|
||||
}
|
||||
rows = rows.push(modes);
|
||||
} else {
|
||||
rows = rows.push(
|
||||
text("Power Profiles service unavailable")
|
||||
shell
|
||||
.render
|
||||
.text("Power Profiles service unavailable")
|
||||
.size(12)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
@@ -102,12 +122,12 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
),
|
||||
] {
|
||||
if enabled {
|
||||
power = power.push(power_tile_action(label, icon, action, theme, p));
|
||||
power = power.push(power_tile_action(shell.render, label, icon, action, theme, p));
|
||||
}
|
||||
}
|
||||
|
||||
if shell.status.reboot || shell.status.poweroff || shell.status.suspend {
|
||||
rows = rows.push(ferese_theme::menus::section_label("Power", shell_font(), muted));
|
||||
rows = rows.push(ferese_theme::menus::section_label("Power", shell.render.font, muted));
|
||||
rows = rows.push(power);
|
||||
}
|
||||
rows
|
||||
@@ -122,8 +142,9 @@ pub(super) fn brightness<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, sty
|
||||
} = style;
|
||||
if let Some(value) = shell.status.brightness {
|
||||
let brightness = column![
|
||||
text("Brightness").size(13),
|
||||
shell.render.text("Brightness").size(13),
|
||||
slider_row(
|
||||
shell.render,
|
||||
ferese_theme::icons::BRIGHTNESS,
|
||||
value,
|
||||
true,
|
||||
@@ -133,12 +154,13 @@ pub(super) fn brightness<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, sty
|
||||
)
|
||||
]
|
||||
.spacing(6);
|
||||
rows = rows.push(control_card(brightness.into(), primary, p));
|
||||
rows = rows.push(control_card(shell.render, brightness.into(), primary, p));
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
fn power_tile<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
label: &'static str,
|
||||
icon: &'static [u8],
|
||||
profile: &'static str,
|
||||
@@ -148,7 +170,7 @@ fn power_tile<'a>(
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let foreground = color_with_opacity(if selected { theme.accent } else { theme.text_primary }, opacity);
|
||||
let tile = container(
|
||||
column![bar_icon(icon, 19, foreground), text(label).size(11)]
|
||||
column![bar_icon(icon, 19, foreground), render.text(label).size(11)]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
@@ -159,10 +181,11 @@ fn power_tile<'a>(
|
||||
.padding([8, 2])
|
||||
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(profile))));
|
||||
|
||||
motion::button(btn, foreground, selected, opacity)
|
||||
motion::button(render, btn, foreground, selected, opacity)
|
||||
}
|
||||
|
||||
fn power_tile_action<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
label: &'static str,
|
||||
icon: &'static [u8],
|
||||
action: Action,
|
||||
@@ -171,7 +194,7 @@ fn power_tile_action<'a>(
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let foreground = color_with_opacity(theme.text_primary, opacity);
|
||||
let tile = container(
|
||||
column![bar_icon(icon, 19, foreground), text(label).size(11)]
|
||||
column![bar_icon(icon, 19, foreground), render.text(label).size(11)]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
@@ -182,5 +205,5 @@ fn power_tile_action<'a>(
|
||||
.padding([8, 2])
|
||||
.on_press(cosmic::Action::App(Message::ConfirmPower(action)));
|
||||
|
||||
motion::button(btn, foreground, false, opacity)
|
||||
motion::button(render, btn, foreground, false, opacity)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use super::bar::status_icon;
|
||||
use super::controls::status_summary;
|
||||
use super::{Menu, MenuRows, MenuStyle};
|
||||
use crate::status::Action;
|
||||
use crate::{FereseShell, Message, text};
|
||||
use crate::{FereseShell, Message};
|
||||
|
||||
const DEVICE_LIST_HEIGHT: f32 = 180.0;
|
||||
|
||||
@@ -14,10 +14,11 @@ fn device_list_height(count: usize) -> Option<f32> {
|
||||
(count > 4).then_some(DEVICE_LIST_HEIGHT)
|
||||
}
|
||||
|
||||
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
||||
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle, in_hub: bool) -> MenuRows<'a> {
|
||||
let MenuStyle { muted, .. } = style;
|
||||
if let Some(b) = &shell.status.bluetooth {
|
||||
rows = rows.push(status_summary(
|
||||
shell.render,
|
||||
status_icon(Menu::Bluetooth, &shell.status).0,
|
||||
if b.enabled {
|
||||
"Bluetooth is on"
|
||||
@@ -41,21 +42,23 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
for device in &b.devices {
|
||||
devices = devices.push(
|
||||
column![
|
||||
text(device).size(14),
|
||||
text("Connected").size(12).class(theme::Text::Color(muted)),
|
||||
shell.render.text(device).size(14),
|
||||
shell.render.text("Connected").size(12).class(theme::Text::Color(muted)),
|
||||
]
|
||||
.spacing(3),
|
||||
);
|
||||
}
|
||||
|
||||
let list: Element<'_, cosmic::Action<Message>> = if let Some(height) = device_list_height(b.devices.len()) {
|
||||
cosmic::iced::widget::scrollable(devices)
|
||||
.height(height)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
devices.into()
|
||||
};
|
||||
// Morph scrolls the whole view, so its device list needs no nested viewport.
|
||||
let list: Element<'_, cosmic::Action<Message>> =
|
||||
if let Some(height) = device_list_height(b.devices.len()).filter(|_| !in_hub) {
|
||||
cosmic::iced::widget::scrollable(devices)
|
||||
.height(height)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
devices.into()
|
||||
};
|
||||
rows = rows.push(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,59 @@ use cosmic::Element;
|
||||
use jiff::Zoned;
|
||||
|
||||
use super::controls::menu_button;
|
||||
use crate::{Message, ShellTheme, color_with_opacity, shell_font};
|
||||
use crate::{Message, ShellTheme, color_with_opacity};
|
||||
|
||||
pub(super) fn view<'a>(offset: i32, theme: ShellTheme, opacity: f32) -> Element<'a, cosmic::Action<Message>> {
|
||||
pub(super) fn view<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
offset: i32,
|
||||
theme: ShellTheme,
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let primary = color_with_opacity(theme.text_primary, opacity);
|
||||
ferese_theme::calendar::grid(
|
||||
Zoned::now().date(),
|
||||
offset,
|
||||
theme.palette(),
|
||||
shell_font(),
|
||||
render.font,
|
||||
opacity,
|
||||
[
|
||||
menu_button("‹", Message::CalendarMonth(-1), primary, opacity),
|
||||
menu_button("Today", Message::CalendarToday, primary, opacity),
|
||||
menu_button("›", Message::CalendarMonth(1), primary, opacity),
|
||||
menu_button(render, "‹", Message::CalendarMonth(-1), primary, opacity),
|
||||
menu_button(render, "Today", Message::CalendarToday, primary, opacity),
|
||||
menu_button(render, "›", Message::CalendarMonth(1), primary, opacity),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn hub<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
offset: i32,
|
||||
theme: ShellTheme,
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let primary = color_with_opacity(theme.text_primary, opacity);
|
||||
let navigation = |label: &'static str, message: Message| {
|
||||
crate::motion::button(
|
||||
render,
|
||||
cosmic::widget::button::custom(render.text(label).size(11).class(cosmic::theme::Text::Color(primary)))
|
||||
.name(label)
|
||||
.padding([2, 4])
|
||||
.height(22)
|
||||
.on_press(cosmic::Action::App(message)),
|
||||
primary,
|
||||
false,
|
||||
opacity,
|
||||
)
|
||||
};
|
||||
ferese_theme::calendar::compact_grid(
|
||||
Zoned::now().date(),
|
||||
offset,
|
||||
theme.palette(),
|
||||
render.font,
|
||||
opacity,
|
||||
[
|
||||
navigation("‹", Message::CalendarMonth(-1)),
|
||||
navigation("Today", Message::CalendarToday),
|
||||
navigation("›", Message::CalendarMonth(1)),
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -4,9 +4,10 @@ use cosmic::widget::{button, column, container, row, slider};
|
||||
use cosmic::{Element, theme};
|
||||
|
||||
use crate::status::Action;
|
||||
use crate::{Message, ShellTheme, accented_icon, color, color_with_opacity, motion, shell_font, text};
|
||||
use crate::{Message, ShellTheme, accented_icon, color, color_with_opacity, motion};
|
||||
|
||||
pub(super) fn status_summary<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
source: &'static [u8],
|
||||
title: &'a str,
|
||||
subtitle: &'a str,
|
||||
@@ -34,13 +35,14 @@ pub(super) fn status_summary<'a>(
|
||||
})),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(16.0).into(),
|
||||
radius: render.radius(16.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
let badge: Element<'_, cosmic::Action<Message>> = if let Some((_, action)) = toggle {
|
||||
motion::button(
|
||||
render,
|
||||
button::custom(badge)
|
||||
.padding(0)
|
||||
.name(if enabled { "Turn off" } else { "Turn on" })
|
||||
@@ -55,8 +57,8 @@ pub(super) fn status_summary<'a>(
|
||||
let summary = row![
|
||||
badge,
|
||||
column![
|
||||
text(title).size(14),
|
||||
text(subtitle).size(12).class(theme::Text::Color(muted)),
|
||||
render.text(title).size(14),
|
||||
render.text(subtitle).size(12).class(theme::Text::Color(muted)),
|
||||
]
|
||||
.spacing(4)
|
||||
.width(Length::Fill)
|
||||
@@ -66,9 +68,14 @@ pub(super) fn status_summary<'a>(
|
||||
summary.into()
|
||||
}
|
||||
|
||||
pub(super) fn level_meter(value: u8, foreground: Color, opacity: f32) -> Element<'static, cosmic::Action<Message>> {
|
||||
pub(super) fn level_meter(
|
||||
render: crate::rendering::Rendering,
|
||||
value: u8,
|
||||
foreground: Color,
|
||||
opacity: f32,
|
||||
) -> Element<'static, cosmic::Action<Message>> {
|
||||
let segment = move |amount: u16, filled: bool| {
|
||||
container(text(""))
|
||||
container(render.text(""))
|
||||
.width(Length::FillPortion(amount))
|
||||
.height(5)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
@@ -82,7 +89,7 @@ pub(super) fn level_meter(value: u8, foreground: Color, opacity: f32) -> Element
|
||||
})),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(2.5).into(),
|
||||
radius: render.radius(2.5).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -100,21 +107,24 @@ pub(super) fn level_meter(value: u8, foreground: Color, opacity: f32) -> Element
|
||||
}
|
||||
|
||||
pub(super) fn control_card<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
content: Element<'a, cosmic::Action<Message>>,
|
||||
foreground: Color,
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
ferese_theme::menus::section(content, foreground, opacity, motion::radius(22.))
|
||||
ferese_theme::menus::section(content, foreground, opacity, render.radius(22.))
|
||||
}
|
||||
|
||||
pub(super) fn menu_button<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
label: &'a str,
|
||||
message: Message,
|
||||
foreground: Color,
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
motion::button(
|
||||
ferese_theme::menus::button(label, cosmic::Action::App(message), shell_font()),
|
||||
render,
|
||||
ferese_theme::menus::button(label, cosmic::Action::App(message), render.font),
|
||||
foreground,
|
||||
false,
|
||||
opacity,
|
||||
@@ -122,6 +132,7 @@ pub(super) fn menu_button<'a>(
|
||||
}
|
||||
|
||||
pub(super) fn shell_switch<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
label: &str,
|
||||
enabled: bool,
|
||||
action: Action,
|
||||
@@ -130,6 +141,7 @@ pub(super) fn shell_switch<'a>(
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let toggle = ferese_theme::menus::switch(enabled, palette.palette(), opacity);
|
||||
motion::button(
|
||||
render,
|
||||
button::custom(toggle)
|
||||
.padding(4)
|
||||
.name(format!("{label}: {}", if enabled { "on" } else { "off" }))
|
||||
@@ -141,16 +153,22 @@ pub(super) fn shell_switch<'a>(
|
||||
}
|
||||
|
||||
pub(super) fn toggle_row<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
label: &'a str,
|
||||
on: bool,
|
||||
action: Action,
|
||||
palette: ShellTheme,
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
ferese_theme::menus::row(label, shell_switch(label, on, action, palette, opacity), shell_font())
|
||||
ferese_theme::menus::row(
|
||||
label,
|
||||
shell_switch(render, label, on, action, palette, opacity),
|
||||
render.font,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn slider_row(
|
||||
render: crate::rendering::Rendering,
|
||||
source: &'static [u8],
|
||||
value: u8,
|
||||
brightness: bool,
|
||||
@@ -171,7 +189,7 @@ pub(super) fn slider_row(
|
||||
row![
|
||||
accented_icon(source, 18, foreground, icon_accent),
|
||||
control,
|
||||
text(format!("{value}%")).size(12).width(36)
|
||||
render.text(format!("{value}%")).size(12).width(36)
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
|
||||
@@ -10,11 +10,11 @@ use super::{Menu, OpenMenu, PopoverAnchor};
|
||||
use crate::status::Action;
|
||||
use crate::{FereseShell, Message};
|
||||
|
||||
fn menu_positioner(rectangle: Rectangle<i32>, height_limit: f32, edge: ferese_config::panel::Edge) -> SctkPositioner {
|
||||
fn menu_positioner(rectangle: Rectangle<i32>, height_limit: f32, edge: ferese_panel::Edge) -> SctkPositioner {
|
||||
// xdg-positioner: bottom/bottom-left for top bars, top/top-left for bottom bars.
|
||||
let (anchor, gravity, gap) = match edge {
|
||||
ferese_config::panel::Edge::Top => (2u32, 6u32, 8),
|
||||
ferese_config::panel::Edge::Bottom => (1u32, 5u32, -8),
|
||||
ferese_panel::Edge::Top => (2u32, 6u32, 8),
|
||||
ferese_panel::Edge::Bottom => (1u32, 5u32, -8),
|
||||
};
|
||||
SctkPositioner {
|
||||
anchor_rect: rectangle,
|
||||
@@ -46,6 +46,9 @@ impl FereseShell {
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if self.panel_for_bar(anchor.parent).style == ferese_panel::PanelStyle::Morph {
|
||||
return self.open_hub(anchor.parent, kind);
|
||||
}
|
||||
if self
|
||||
.menu
|
||||
.as_ref()
|
||||
@@ -274,7 +277,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn menus_open_toward_the_desktop_from_either_panel_edge() {
|
||||
use ferese_config::panel::Edge;
|
||||
use ferese_panel::Edge;
|
||||
let rectangle = Rectangle {
|
||||
x: 400,
|
||||
y: 0,
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde_json::json;
|
||||
|
||||
use super::{Menu, MenuRows, MenuStyle};
|
||||
use crate::{
|
||||
BarMetrics, FereseShell, Message, ShellTheme, accented_icon, bar_content, color, color_with_opacity, motion, text,
|
||||
BarMetrics, FereseShell, Message, ShellTheme, accented_icon, bar_content, color, color_with_opacity, motion,
|
||||
};
|
||||
|
||||
const SEEK_THUMB_DIAMETER: f32 = 10.0;
|
||||
@@ -78,7 +78,7 @@ pub(super) fn bar<'a>(
|
||||
width: Option<f32>,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let Some(player) = &shell.media.snapshot.selected else {
|
||||
return text("").into();
|
||||
return shell.render.text("").into();
|
||||
};
|
||||
let foreground = color(if selected { palette.accent } else { palette.text_primary });
|
||||
let height = (metrics.group_item_height - 4.0).max(0.0);
|
||||
@@ -89,7 +89,7 @@ pub(super) fn bar<'a>(
|
||||
.width(side)
|
||||
.height(side)
|
||||
.content_fit(cosmic::iced::ContentFit::Cover)
|
||||
.border_radius(motion::radius(6.0))
|
||||
.border_radius(shell.render.radius(6.0))
|
||||
.shape(BorderShape::Continuous)
|
||||
.into(),
|
||||
None => container(accented_icon(
|
||||
@@ -103,7 +103,9 @@ pub(super) fn bar<'a>(
|
||||
.width(side)
|
||||
.into(),
|
||||
};
|
||||
let label = text(player.label())
|
||||
let label = shell
|
||||
.render
|
||||
.text(player.label())
|
||||
.size(metrics.text_size)
|
||||
.wrapping(cosmic::iced::widget::text::Wrapping::None)
|
||||
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
||||
@@ -170,8 +172,8 @@ pub(super) fn bar<'a>(
|
||||
|
||||
let controls = container(
|
||||
row![
|
||||
motion::button(details, foreground, false, 1.0),
|
||||
motion::button(toggle, foreground, false, 1.0)
|
||||
motion::button(shell.render, details, foreground, false, 1.0),
|
||||
motion::button(shell.render, toggle, foreground, false, 1.0)
|
||||
]
|
||||
.spacing(2)
|
||||
.align_y(Alignment::Center),
|
||||
@@ -211,6 +213,18 @@ fn transport<'a>(
|
||||
action: &str,
|
||||
enabled: bool,
|
||||
style: MenuStyle,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
transport_sized(shell, source, label, action, enabled, style, 44.)
|
||||
}
|
||||
|
||||
fn transport_sized<'a>(
|
||||
shell: &'a FereseShell,
|
||||
source: &'static [u8],
|
||||
label: &'static str,
|
||||
action: &str,
|
||||
enabled: bool,
|
||||
style: MenuStyle,
|
||||
size: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let fill = (action == "play-pause").then_some(if enabled { style.primary } else { style.muted });
|
||||
let foreground = match fill {
|
||||
@@ -227,19 +241,142 @@ fn transport<'a>(
|
||||
}
|
||||
};
|
||||
let mut control = button::custom(
|
||||
container(accented_icon(source, 24, foreground, color(style.theme.accent)))
|
||||
.center_x(24)
|
||||
.center_y(24),
|
||||
container(
|
||||
accented_icon(
|
||||
source,
|
||||
(size - 20.) as u16,
|
||||
cosmic::iced::Color { a: 1., ..foreground },
|
||||
color(style.theme.accent),
|
||||
)
|
||||
.opacity(foreground.a),
|
||||
)
|
||||
.center_x(size - 20.)
|
||||
.center_y(size - 20.),
|
||||
)
|
||||
.padding(10)
|
||||
.width(44)
|
||||
.height(44)
|
||||
.width(size)
|
||||
.height(size)
|
||||
.name(label);
|
||||
if enabled {
|
||||
control = control.on_press(cosmic::Action::App(Message::MediaAction(shell.media.action(action))));
|
||||
}
|
||||
|
||||
motion::circular_button(control, foreground, fill, style.opacity, 44.0)
|
||||
motion::circular_button(shell.render, control, foreground, fill, style.opacity, size)
|
||||
}
|
||||
|
||||
pub(super) fn hub<'a>(shell: &'a FereseShell, rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
||||
let Some(player) = &shell.media.snapshot.selected else {
|
||||
return rows.push(
|
||||
shell
|
||||
.render
|
||||
.text("No active player")
|
||||
.size(14)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
};
|
||||
let cover: Element<'_, cosmic::Action<Message>> =
|
||||
match shell.media.artwork.as_ref().and_then(|art| art.handle.as_ref()) {
|
||||
Some(handle) => image(handle.clone())
|
||||
.width(64)
|
||||
.height(64)
|
||||
.opacity(style.opacity)
|
||||
.content_fit(cosmic::iced::ContentFit::Cover)
|
||||
.border_radius(10.)
|
||||
.into(),
|
||||
None => container(
|
||||
accented_icon(
|
||||
icons::MEDIA,
|
||||
26,
|
||||
color(style.theme.text_primary),
|
||||
color(style.theme.accent),
|
||||
)
|
||||
.opacity(style.opacity),
|
||||
)
|
||||
.center_x(64)
|
||||
.center_y(64)
|
||||
.class(theme::Container::custom(move |_| {
|
||||
ferese_theme::controls::surface_appearance(style.primary.scale_alpha(0.06), 10.)
|
||||
}))
|
||||
.into(),
|
||||
};
|
||||
let mut info = column![
|
||||
shell
|
||||
.render
|
||||
.text(player.label())
|
||||
.size(14)
|
||||
.class(theme::Text::Color(style.primary))
|
||||
.width(Length::Fill)
|
||||
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
||||
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1)
|
||||
)),
|
||||
shell
|
||||
.render
|
||||
.text(&player.artist)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(style.muted))
|
||||
.width(Length::Fill)
|
||||
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
||||
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1)
|
||||
))
|
||||
]
|
||||
.spacing(4)
|
||||
.width(Length::Fill);
|
||||
if let (Some(length), Some(position)) = (
|
||||
player.length_us.filter(|length| *length > 0),
|
||||
player.position_at(shell.media.now_us),
|
||||
) {
|
||||
let value =
|
||||
(shell.media.seeking.unwrap_or(position).min(length) as f64 / length as f64 * 10_000.).round() as u32;
|
||||
if player.can_control && player.can_seek && player.track_id.is_some() {
|
||||
info = info.push(
|
||||
slider(0..=10_000_u32, value, move |value| {
|
||||
cosmic::Action::App(Message::MediaSeek(seek_position(length, value)))
|
||||
})
|
||||
.on_release(cosmic::Action::App(Message::MediaSeekCommit))
|
||||
.height(16)
|
||||
.handle_width(SEEK_THUMB_DIAMETER)
|
||||
.class(seek_slider_style(style))
|
||||
.width(Length::Fill),
|
||||
);
|
||||
}
|
||||
}
|
||||
let rows = rows.push(row![cover, info].spacing(12).align_y(Alignment::Center));
|
||||
let playing = player.status == Playback::Playing;
|
||||
rows.push(
|
||||
container(
|
||||
row![
|
||||
transport_sized(
|
||||
shell,
|
||||
icons::MEDIA_PREVIOUS,
|
||||
"Previous track",
|
||||
"previous",
|
||||
player.can_control && player.can_previous,
|
||||
style,
|
||||
36.
|
||||
),
|
||||
transport_sized(
|
||||
shell,
|
||||
if playing { icons::MEDIA_PAUSE } else { icons::MEDIA_PLAY },
|
||||
if playing { "Pause" } else { "Play" },
|
||||
"play-pause",
|
||||
player.can_toggle(),
|
||||
style,
|
||||
36.
|
||||
),
|
||||
transport_sized(
|
||||
shell,
|
||||
icons::MEDIA_NEXT,
|
||||
"Next track",
|
||||
"next",
|
||||
player.can_control && player.can_next,
|
||||
style,
|
||||
36.
|
||||
),
|
||||
]
|
||||
.spacing(8),
|
||||
)
|
||||
.center_x(Length::Fill),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
||||
@@ -250,7 +387,7 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
.width(64)
|
||||
.height(64)
|
||||
.content_fit(cosmic::iced::ContentFit::Contain)
|
||||
.border_radius(motion::radius(12.0))
|
||||
.border_radius(shell.render.radius(12.0))
|
||||
.shape(BorderShape::Continuous)
|
||||
.into(),
|
||||
None => container(accented_icon(icons::MEDIA, 30, style.muted, color(style.theme.accent)))
|
||||
@@ -259,14 +396,23 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
.into(),
|
||||
};
|
||||
let text_size = if player.label().len() >= 24 { 13 } else { 15 };
|
||||
let mut info = column![text(player.label()).size(text_size).width(Length::Fill).ellipsize(
|
||||
cosmic::iced::widget::text::Ellipsize::End(cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(2))
|
||||
)]
|
||||
let mut info = column![
|
||||
shell
|
||||
.render
|
||||
.text(player.label())
|
||||
.size(text_size)
|
||||
.width(Length::Fill)
|
||||
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
||||
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(2)
|
||||
))
|
||||
]
|
||||
.spacing(4)
|
||||
.width(Length::Fill);
|
||||
if !player.artist.is_empty() {
|
||||
info = info.push(
|
||||
text(&player.artist)
|
||||
shell
|
||||
.render
|
||||
.text(&player.artist)
|
||||
.size(12)
|
||||
.width(Length::Fill)
|
||||
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
||||
@@ -277,7 +423,9 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
}
|
||||
if !player.album.is_empty() {
|
||||
info = info.push(
|
||||
text(&player.album)
|
||||
shell
|
||||
.render
|
||||
.text(&player.album)
|
||||
.size(11)
|
||||
.width(Length::Fill)
|
||||
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
||||
@@ -342,12 +490,16 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
|
||||
progress = progress.push(
|
||||
row![
|
||||
text(time_label(position))
|
||||
shell
|
||||
.render
|
||||
.text(time_label(position))
|
||||
.font(cosmic::iced::Font::MONOSPACE)
|
||||
.size(11)
|
||||
.class(theme::Text::Color(style.muted))
|
||||
.width(Length::Fill),
|
||||
text(time_label(length))
|
||||
shell
|
||||
.render
|
||||
.text(time_label(length))
|
||||
.font(cosmic::iced::Font::MONOSPACE)
|
||||
.size(11)
|
||||
.class(theme::Text::Color(style.muted))
|
||||
@@ -398,7 +550,8 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
player.identity.clone()
|
||||
};
|
||||
rows = rows.push(motion::button(
|
||||
button::custom(text(label).size(12))
|
||||
shell.render,
|
||||
button::custom(shell.render.text(label).size(12))
|
||||
.padding([6, 8])
|
||||
.on_press(cosmic::Action::App(Message::MediaChoose)),
|
||||
style.primary,
|
||||
@@ -406,12 +559,19 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
style.opacity,
|
||||
));
|
||||
} else {
|
||||
rows = rows.push(text("No active player").size(13).class(theme::Text::Color(style.muted)));
|
||||
rows = rows.push(
|
||||
shell
|
||||
.render
|
||||
.text("No active player")
|
||||
.size(13)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
}
|
||||
|
||||
if shell.media.choosing || shell.media.snapshot.selected.is_none() {
|
||||
let mut choices = column::with_capacity(shell.media.snapshot.players.len() + 1).spacing(4);
|
||||
choices = choices.push(super::controls::menu_button(
|
||||
shell.render,
|
||||
"Choose automatically",
|
||||
Message::MediaAction(json!({"action":"auto"})),
|
||||
style.primary,
|
||||
@@ -432,12 +592,14 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
choices = choices.push(
|
||||
row![
|
||||
super::controls::menu_button(
|
||||
shell.render,
|
||||
label,
|
||||
Message::MediaAction(pin),
|
||||
if player.ignored { style.muted } else { style.primary },
|
||||
style.opacity
|
||||
),
|
||||
super::controls::menu_button(
|
||||
shell.render,
|
||||
if player.ignored { "Show" } else { "Ignore" },
|
||||
Message::MediaAction(ignore),
|
||||
style.muted,
|
||||
@@ -452,7 +614,7 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
}
|
||||
|
||||
if let Some(error) = &shell.media.error {
|
||||
rows = rows.push(text(error).size(12).class(theme::Text::Color(style.muted)));
|
||||
rows = rows.push(shell.render.text(error).size(12).class(theme::Text::Color(style.muted)));
|
||||
}
|
||||
|
||||
rows
|
||||
|
||||
@@ -32,6 +32,7 @@ struct MenuStyle {
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Menu {
|
||||
Workspaces,
|
||||
Network,
|
||||
Bluetooth,
|
||||
Audio,
|
||||
@@ -74,6 +75,7 @@ impl Menu {
|
||||
|
||||
fn title(self) -> &'static str {
|
||||
match self {
|
||||
Self::Workspaces => "Workspaces",
|
||||
Self::System => "Control Center",
|
||||
Self::Overflow => "Panel items",
|
||||
Self::Network => "Wi-Fi",
|
||||
@@ -95,7 +97,7 @@ impl Menu {
|
||||
Self::Battery => status.battery.is_some(),
|
||||
Self::Calendar | Self::Recording | Self::Media => true,
|
||||
Self::Notifications => status.notifications.is_some(),
|
||||
Self::System | Self::Overflow => true,
|
||||
Self::System | Self::Overflow | Self::Workspaces => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,8 +163,151 @@ impl OpenMenu {
|
||||
}
|
||||
|
||||
impl FereseShell {
|
||||
pub(crate) fn view_hub_menu(&self, kind: Menu, opacity: f32) -> Element<'_, cosmic::Action<Message>> {
|
||||
let theme = self.config.theme;
|
||||
let style = MenuStyle {
|
||||
theme,
|
||||
primary: color_with_opacity(theme.text_primary, opacity),
|
||||
muted: color_with_opacity(theme.text_muted, opacity),
|
||||
opacity,
|
||||
};
|
||||
let mut rows = column::with_capacity(8).spacing(8).width(Length::Fill);
|
||||
if kind == Menu::Network
|
||||
&& let Some(network) = &self.status.network
|
||||
{
|
||||
rows = rows.push(toggle_row(
|
||||
self.render,
|
||||
"Wi-Fi",
|
||||
network.enabled,
|
||||
Action::Wifi(!network.enabled),
|
||||
theme,
|
||||
opacity,
|
||||
));
|
||||
}
|
||||
if kind == Menu::Audio
|
||||
&& let Some(audio) = &self.status.audio
|
||||
{
|
||||
rows = rows.push(toggle_row(
|
||||
self.render,
|
||||
"Mute",
|
||||
audio.muted,
|
||||
Action::Mute(!audio.muted),
|
||||
theme,
|
||||
opacity,
|
||||
));
|
||||
}
|
||||
let mut rows = match kind {
|
||||
Menu::System => system::view(self, rows, style, true),
|
||||
Menu::Network => network::view(self, rows, style),
|
||||
Menu::Bluetooth => bluetooth::view(self, rows, style, true),
|
||||
Menu::Audio => audio::view(self, rows, style, false),
|
||||
Menu::Battery => battery::view(self, rows, style),
|
||||
Menu::Calendar => rows.push(calendar::hub(self.render, self.calendar_offset, theme, opacity)),
|
||||
Menu::Media => media::hub(self, rows, style),
|
||||
Menu::Notifications => {
|
||||
let mut rows = notification_controls(self, rows, style, true);
|
||||
if self.notifications.entries.is_empty() {
|
||||
rows = rows.push(
|
||||
self.render
|
||||
.text("No notifications")
|
||||
.size(13)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
} else {
|
||||
rows = rows.push(menu_button(
|
||||
self.render,
|
||||
"Clear notifications",
|
||||
Message::ClearNotifications,
|
||||
style.muted,
|
||||
opacity,
|
||||
));
|
||||
for notice in &self.notifications.entries {
|
||||
rows = rows.push(control_card(
|
||||
self.render,
|
||||
column![
|
||||
row![
|
||||
self.render
|
||||
.text(notice.app.clone())
|
||||
.size(11)
|
||||
.class(theme::Text::Color(style.muted))
|
||||
.width(Length::Fill),
|
||||
menu_button(
|
||||
self.render,
|
||||
"×",
|
||||
Message::RemoveNotification(notice.id),
|
||||
style.muted,
|
||||
opacity
|
||||
)
|
||||
],
|
||||
self.render
|
||||
.text(notice.title.clone())
|
||||
.size(13)
|
||||
.class(theme::Text::Color(style.primary)),
|
||||
self.render
|
||||
.text(notice.body.clone())
|
||||
.size(12)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
]
|
||||
.spacing(4)
|
||||
.into(),
|
||||
style.primary,
|
||||
opacity,
|
||||
));
|
||||
for (action, label) in ¬ice.actions {
|
||||
rows = rows.push(menu_button(
|
||||
self.render,
|
||||
label,
|
||||
Message::InvokeNotification(notice.id, action.clone()),
|
||||
style.primary,
|
||||
opacity,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
rows
|
||||
}
|
||||
Menu::Recording => rows.push(menu_button(
|
||||
self.render,
|
||||
if self.recorder.busy() {
|
||||
"Stop recording"
|
||||
} else {
|
||||
"Record screen"
|
||||
},
|
||||
if self.recorder.busy() {
|
||||
Message::StopRecording
|
||||
} else {
|
||||
Message::StartRecording
|
||||
},
|
||||
style.primary,
|
||||
opacity,
|
||||
)),
|
||||
Menu::Overflow | Menu::Workspaces => rows,
|
||||
};
|
||||
if !kind.available(&self.status) {
|
||||
rows = rows.push(
|
||||
self.render
|
||||
.text("Service unavailable")
|
||||
.size(13)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
}
|
||||
if let Some(error) = self.status_error.as_ref().and_then(|error| kind.action_error(error)) {
|
||||
rows = rows.push(self.render.text(error).size(12).class(theme::Text::Color(style.muted)));
|
||||
}
|
||||
container(rows)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
text_color: Some(style.primary),
|
||||
icon_color: Some(style.primary),
|
||||
..Default::default()
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_status_menu(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||
let Some(menu) = &self.menu else { return text("").into() };
|
||||
let Some(menu) = &self.menu else {
|
||||
return self.render.text("").into();
|
||||
};
|
||||
motion::frame_driven(
|
||||
menu.motion.revision(),
|
||||
|now| self.view_status_menu_at(now),
|
||||
@@ -186,7 +331,7 @@ impl FereseShell {
|
||||
|
||||
fn view_status_menu_at(&self, now: Instant) -> Element<'_, cosmic::Action<Message>> {
|
||||
let Some(menu) = &self.menu else {
|
||||
return text("").into();
|
||||
return self.render.text("").into();
|
||||
};
|
||||
if menu.kind == Menu::Notifications && self.notifications.ready {
|
||||
return cosmic::widget::autosize::autosize(
|
||||
@@ -217,9 +362,9 @@ impl FereseShell {
|
||||
.into(),
|
||||
color(theme.accent),
|
||||
p,
|
||||
motion::radius(20.),
|
||||
self.render.radius(20.),
|
||||
);
|
||||
let mut heading = ferese_theme::menus::heading(badge, kind.title(), shell_font());
|
||||
let mut heading = ferese_theme::menus::heading(badge, kind.title(), self.render.font);
|
||||
let style = MenuStyle {
|
||||
theme,
|
||||
primary,
|
||||
@@ -237,19 +382,24 @@ impl FereseShell {
|
||||
rows = rows.push(heading);
|
||||
}
|
||||
rows = match kind {
|
||||
Menu::System => system::view(self, rows, style),
|
||||
Menu::System => system::view(self, rows, style, false),
|
||||
Menu::Network => network::view(self, rows, style),
|
||||
Menu::Bluetooth => bluetooth::view(self, rows, style),
|
||||
Menu::Bluetooth => bluetooth::view(self, rows, style, false),
|
||||
Menu::Audio => audio::view(self, rows, style, false),
|
||||
Menu::Battery => battery::view(self, rows, style),
|
||||
Menu::Calendar => rows.push(calendar::view(self.calendar_offset, theme, p)),
|
||||
Menu::Calendar => rows.push(calendar::view(self.render, self.calendar_offset, theme, p)),
|
||||
Menu::Notifications => notification_controls(self, rows, style, false),
|
||||
Menu::Recording => rows,
|
||||
Menu::Recording | Menu::Workspaces => rows,
|
||||
Menu::Overflow => self.view_panel_overflow(rows),
|
||||
Menu::Media => media::view(self, rows, style),
|
||||
};
|
||||
if kind != Menu::Calendar && !kind.available(&self.status) {
|
||||
rows = rows.push(text("Service unavailable").size(13).class(theme::Text::Color(muted)));
|
||||
rows = rows.push(
|
||||
self.render
|
||||
.text("Service unavailable")
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(error) = self
|
||||
@@ -257,7 +407,7 @@ impl FereseShell {
|
||||
.as_ref()
|
||||
.and_then(|error| menu.kind.action_error(error))
|
||||
{
|
||||
rows = rows.push(text(error).size(12).class(theme::Text::Color(Color {
|
||||
rows = rows.push(self.render.text(error).size(12).class(theme::Text::Color(Color {
|
||||
a: p,
|
||||
..Color::from_rgb8(230, 172, 90)
|
||||
})));
|
||||
@@ -322,9 +472,9 @@ fn notification_controls<'a>(
|
||||
opacity: p,
|
||||
} = style;
|
||||
if let Some(n) = &shell.status.notifications {
|
||||
let toggle = toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
|
||||
let toggle = toggle_row(shell.render, "Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
|
||||
rows = rows.push(if combined {
|
||||
control_card(toggle, primary, p)
|
||||
control_card(shell.render, toggle, primary, p)
|
||||
} else {
|
||||
toggle
|
||||
});
|
||||
@@ -332,17 +482,20 @@ fn notification_controls<'a>(
|
||||
if !combined {
|
||||
rows = rows
|
||||
.push(
|
||||
text(if n.count == 0 {
|
||||
"No notifications".to_owned()
|
||||
} else if n.count == 1 {
|
||||
"1 notification".to_owned()
|
||||
} else {
|
||||
format!("{} notifications", n.count)
|
||||
})
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
shell
|
||||
.render
|
||||
.text(if n.count == 0 {
|
||||
"No notifications".to_owned()
|
||||
} else if n.count == 1 {
|
||||
"1 notification".to_owned()
|
||||
} else {
|
||||
format!("{} notifications", n.count)
|
||||
})
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
)
|
||||
.push(menu_button(
|
||||
shell.render,
|
||||
"Open notification center",
|
||||
Message::Control(Action::Notifications),
|
||||
primary,
|
||||
|
||||
@@ -4,8 +4,8 @@ use cosmic::widget::{column, row};
|
||||
use super::bar::status_icon;
|
||||
use super::controls::{level_meter, shell_switch, status_summary};
|
||||
use super::{Menu, MenuRows, MenuStyle};
|
||||
use crate::FereseShell;
|
||||
use crate::status::Action;
|
||||
use crate::{FereseShell, text};
|
||||
|
||||
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
||||
let MenuStyle {
|
||||
@@ -14,6 +14,7 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
if let Some(n) = &shell.status.network {
|
||||
let connected = n.enabled && n.connection.is_some();
|
||||
rows = rows.push(status_summary(
|
||||
shell.render,
|
||||
status_icon(Menu::Network, &shell.status).0,
|
||||
if !n.enabled {
|
||||
"Wi-Fi is off"
|
||||
@@ -41,10 +42,10 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
rows = rows.push(
|
||||
column![
|
||||
row![
|
||||
text("Signal strength").size(12).width(Length::Fill),
|
||||
text(strength).size(12)
|
||||
shell.render.text("Signal strength").size(12).width(Length::Fill),
|
||||
shell.render.text(strength).size(12)
|
||||
],
|
||||
level_meter(n.signal, primary, p),
|
||||
level_meter(shell.render, n.signal, primary, p),
|
||||
]
|
||||
.spacing(8),
|
||||
);
|
||||
@@ -61,6 +62,7 @@ pub(super) fn heading<'a>(
|
||||
let MenuStyle { theme, opacity: p, .. } = style;
|
||||
if let Some(network) = &shell.status.network {
|
||||
heading = heading.push(shell_switch(
|
||||
shell.render,
|
||||
"Wi-Fi",
|
||||
network.enabled,
|
||||
Action::Wifi(!network.enabled),
|
||||
|
||||
@@ -6,9 +6,14 @@ use super::bar::status_icon;
|
||||
use super::controls::control_card;
|
||||
use super::{Menu, MenuRows, MenuStyle, audio, battery, notification_controls};
|
||||
use crate::status::{self, Action};
|
||||
use crate::{FereseShell, Message, ShellTheme, accented_icon, color, color_with_opacity, motion, shell_font, text};
|
||||
use crate::{FereseShell, Message, accented_icon, color, color_with_opacity, motion};
|
||||
|
||||
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
||||
pub(super) fn view<'a>(
|
||||
shell: &'a FereseShell,
|
||||
mut rows: MenuRows<'a>,
|
||||
style: MenuStyle,
|
||||
show_settings_link: bool,
|
||||
) -> MenuRows<'a> {
|
||||
let MenuStyle {
|
||||
theme,
|
||||
primary,
|
||||
@@ -19,7 +24,8 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
let mut modes = row([]).spacing(6);
|
||||
for (mode, label) in [(Mode::Light, "☀ Light"), (Mode::Dark, "☾ Dark"), (Mode::Auto, "◐ Auto")] {
|
||||
modes = modes.push(motion::button(
|
||||
button::custom(text(label).font(shell_font()).size(13))
|
||||
shell.render,
|
||||
button::custom(shell.render.text(label).font(shell.render.font).size(13))
|
||||
.width(Length::Fill)
|
||||
.padding(9)
|
||||
.on_press(cosmic::Action::App(Message::ThemeMode(mode))),
|
||||
@@ -28,23 +34,54 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
p,
|
||||
));
|
||||
}
|
||||
rows = rows.push(modes);
|
||||
if show_settings_link
|
||||
&& shell
|
||||
.config
|
||||
.status
|
||||
.settings_command
|
||||
.as_ref()
|
||||
.is_some_and(|command| !command.is_empty())
|
||||
{
|
||||
let settings = motion::button(
|
||||
shell.render,
|
||||
button::custom(
|
||||
ferese_theme::icons::tinted(ferese_theme::icons::SETTINGS, 18, color(theme.text_primary)).opacity(p),
|
||||
)
|
||||
.name("Open Ferese Settings")
|
||||
.width(30)
|
||||
.height(28)
|
||||
.padding([5, 3])
|
||||
.on_press(cosmic::Action::App(Message::Control(Action::Settings))),
|
||||
primary,
|
||||
false,
|
||||
p,
|
||||
);
|
||||
modes = modes.push(cosmic::widget::tooltip(
|
||||
settings,
|
||||
shell.render.text("Open Ferese Settings").size(12),
|
||||
cosmic::widget::tooltip::Position::Left,
|
||||
));
|
||||
}
|
||||
rows = rows.push(modes.align_y(Alignment::Center));
|
||||
if shell.config.theme_mode == Mode::Auto {
|
||||
rows = rows.push(
|
||||
text(if theme.appearance == Appearance::Light {
|
||||
"Auto · currently light"
|
||||
} else {
|
||||
"Auto · currently dark"
|
||||
})
|
||||
.font(shell_font())
|
||||
.size(12)
|
||||
.class(cosmic::theme::Text::Color(muted)),
|
||||
shell
|
||||
.render
|
||||
.text(if theme.appearance == Appearance::Light {
|
||||
"Auto · currently light"
|
||||
} else {
|
||||
"Auto · currently dark"
|
||||
})
|
||||
.font(shell.render.font)
|
||||
.size(12)
|
||||
.class(cosmic::theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
let mut connections = row::with_capacity(3).spacing(8).width(Length::Fill);
|
||||
|
||||
if let Some(n) = &shell.status.network {
|
||||
connections = connections.push(connection_control(
|
||||
shell.render,
|
||||
"Wi-Fi",
|
||||
if n.enabled {
|
||||
n.connection.as_deref().unwrap_or("Not connected")
|
||||
@@ -54,13 +91,13 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
status_icon(Menu::Network, &shell.status).0,
|
||||
n.enabled,
|
||||
Some(Action::Wifi(!n.enabled)),
|
||||
theme,
|
||||
p,
|
||||
style,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(b) = &shell.status.bluetooth {
|
||||
connections = connections.push(connection_control(
|
||||
shell.render,
|
||||
"Bluetooth",
|
||||
if b.enabled {
|
||||
b.devices.first().map(String::as_str).unwrap_or("On")
|
||||
@@ -70,25 +107,24 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
||||
status_icon(Menu::Bluetooth, &shell.status).0,
|
||||
b.enabled,
|
||||
Some(Action::Bluetooth(!b.enabled)),
|
||||
theme,
|
||||
p,
|
||||
style,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(b) = &shell.status.battery {
|
||||
connections = connections.push(connection_control(
|
||||
shell.render,
|
||||
"Battery",
|
||||
&format!("{}%", b.percent),
|
||||
status_icon(Menu::Battery, &shell.status).0,
|
||||
b.status == "Charging",
|
||||
None,
|
||||
theme,
|
||||
p,
|
||||
style,
|
||||
));
|
||||
}
|
||||
|
||||
if shell.status.network.is_some() || shell.status.bluetooth.is_some() || shell.status.battery.is_some() {
|
||||
rows = rows.push(control_card(connections.into(), primary, p));
|
||||
rows = rows.push(control_card(shell.render, connections.into(), primary, p));
|
||||
}
|
||||
rows = audio::view(shell, rows, style, true);
|
||||
rows = battery::brightness(shell, rows, style);
|
||||
@@ -109,14 +145,19 @@ fn connection_caption(detail: &str) -> String {
|
||||
}
|
||||
|
||||
fn connection_control<'a>(
|
||||
render: crate::rendering::Rendering,
|
||||
label: &'static str,
|
||||
detail: &str,
|
||||
source: &'static [u8],
|
||||
enabled: bool,
|
||||
action: Option<Action>,
|
||||
palette: ShellTheme,
|
||||
opacity: f32,
|
||||
style: MenuStyle,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let MenuStyle {
|
||||
theme: palette,
|
||||
opacity,
|
||||
..
|
||||
} = style;
|
||||
let selected = enabled && action.is_some();
|
||||
let (fill, on_accent) = ferese_theme::accent_pair(
|
||||
ferese_theme::composite(color(palette.accent), color(palette.surface_base)),
|
||||
@@ -143,7 +184,8 @@ fn connection_control<'a>(
|
||||
.align_y(alignment::Vertical::Center);
|
||||
let content = column![icon].spacing(2).width(Length::Fill).align_x(Alignment::Center);
|
||||
let content = content.push(
|
||||
text(connection_caption(detail))
|
||||
render
|
||||
.text(connection_caption(detail))
|
||||
.size(11)
|
||||
.width(Length::Fill)
|
||||
.height(16)
|
||||
@@ -167,12 +209,12 @@ fn connection_control<'a>(
|
||||
.class(ferese_theme::controls::filled_button(
|
||||
fill,
|
||||
foreground,
|
||||
motion::radius(14.),
|
||||
render.radius(14.),
|
||||
opacity,
|
||||
))
|
||||
.into()
|
||||
} else {
|
||||
motion::button(button, foreground, false, opacity)
|
||||
motion::button(render, button, foreground, false, opacity)
|
||||
};
|
||||
container(tile).width(Length::FillPortion(1)).into()
|
||||
}
|
||||
@@ -197,6 +239,7 @@ pub(super) fn heading<'a>(
|
||||
.is_some_and(|program| status::available(program))
|
||||
{
|
||||
heading = heading.push(motion::button(
|
||||
shell.render,
|
||||
button::custom(accented_icon(
|
||||
ferese_theme::icons::SETTINGS,
|
||||
20,
|
||||
|
||||
Reference in new issue
Block a user