Centralize native palettes, presets, typography, controls, menus, calendar rendering, and icon assets in ferese-theme. Reuse the shared components across the shell, settings, lock screen, authentication prompt, and screen-sharing chooser. Attach themed dialogs to compositor-owned materials, correct backdrop capture and sampling across output transforms, smooth patterned backgrounds, and prevent clipped picker buttons with the configured shell font.
850 lines
32 KiB
Rust
850 lines
32 KiB
Rust
use super::*;
|
|
use cosmic::iced::widget::scrollable::{Direction, Scrollbar};
|
|
use cosmic::widget::{column, scrollable};
|
|
|
|
const POPUP_WIDTH: u32 = 368;
|
|
|
|
fn popup_height(notice: ¬ifications::Notice, count: usize) -> u32 {
|
|
let body = if notice.body.is_empty() && count == 1 {
|
|
0
|
|
} else {
|
|
22
|
|
};
|
|
let actions = if notice.live && notice.actions.iter().any(|(key, _)| key != "default") {
|
|
39
|
|
} else {
|
|
0
|
|
};
|
|
76 + body + actions + if count > 1 { 4 } else { 0 }
|
|
}
|
|
|
|
fn history_height(center: ¬ifications::Center) -> u32 {
|
|
if center.entries.is_empty() {
|
|
return 280;
|
|
}
|
|
let groups = center.history_groups();
|
|
let card_height = |notice: ¬ifications::Notice, count| {
|
|
let wrapped_lines = if count == 1 {
|
|
notice
|
|
.body
|
|
.lines()
|
|
.map(|line| line.chars().count().div_ceil(40).max(1))
|
|
.sum::<usize>()
|
|
} else {
|
|
1
|
|
};
|
|
popup_height(notice, count)
|
|
+ wrapped_lines.saturating_sub(1) as u32 * 17
|
|
+ if count > 1 { 12 } else { 0 }
|
|
};
|
|
let cards: u32 = groups
|
|
.iter()
|
|
.map(|group| {
|
|
if group.len() > 1 && center.expanded_apps.contains(&group[0].app) {
|
|
32 + group
|
|
.iter()
|
|
.map(|notice| card_height(notice, 1))
|
|
.sum::<u32>()
|
|
+ group.len().saturating_sub(1) as u32 * 8
|
|
} else {
|
|
card_height(group[0], group.len())
|
|
}
|
|
})
|
|
.sum();
|
|
// Header, DND row and section tools share the same compact rhythm as the
|
|
// cards. Long histories scroll; short histories do not reserve blank space.
|
|
(170 + cards + groups.len().saturating_sub(1) as u32 * 8).min(620)
|
|
}
|
|
|
|
fn blend(base: Color, foreground: Color, amount: f32) -> Color {
|
|
Color {
|
|
r: base.r + (foreground.r - base.r) * amount,
|
|
g: base.g + (foreground.g - base.g) * amount,
|
|
b: base.b + (foreground.b - base.b) * amount,
|
|
a: base.a,
|
|
}
|
|
}
|
|
|
|
fn age_label(age: Duration) -> String {
|
|
match age.as_secs() {
|
|
0..60 => "now".into(),
|
|
60..3600 => format!("{}m ago", age.as_secs() / 60),
|
|
3600..86400 => format!("{}h ago", age.as_secs() / 3600),
|
|
seconds => format!("{}d ago", seconds / 86400),
|
|
}
|
|
}
|
|
|
|
fn card_button<'a>(
|
|
content: impl Into<Element<'a, cosmic::Action<Message>>>,
|
|
message: Message,
|
|
foreground: Color,
|
|
hover: Color,
|
|
round: bool,
|
|
accessible_name: String,
|
|
) -> Element<'a, cosmic::Action<Message>> {
|
|
button::custom(content)
|
|
.name(accessible_name)
|
|
.width(if round { Length::Shrink } else { Length::Fill })
|
|
.padding(0)
|
|
.on_press(cosmic::Action::App(message))
|
|
.class(ferese_theme::controls::notification_button(
|
|
foreground,
|
|
hover,
|
|
if round { motion::radius(11.) } else { 0. },
|
|
round,
|
|
))
|
|
.into()
|
|
}
|
|
|
|
fn icon_control<'a>(
|
|
source: &'static [u8],
|
|
label: &'static str,
|
|
message: Message,
|
|
foreground: Color,
|
|
background: Color,
|
|
extent: u16,
|
|
) -> Element<'a, cosmic::Action<Message>> {
|
|
let control = card_button(
|
|
container(bar_icon(source, 14, foreground))
|
|
.center_x(extent)
|
|
.center_y(extent),
|
|
message,
|
|
foreground,
|
|
background,
|
|
true,
|
|
label.into(),
|
|
);
|
|
cosmic::widget::tooltip(
|
|
control,
|
|
text(label).size(11).class(theme::Text::Color(foreground)),
|
|
cosmic::widget::tooltip::Position::Left,
|
|
)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: Some(Background::Color(background)),
|
|
border: Border {
|
|
radius: motion::radius(12.0).into(),
|
|
..Default::default()
|
|
},
|
|
text_color: Some(foreground),
|
|
..Default::default()
|
|
}))
|
|
.into()
|
|
}
|
|
|
|
pub(super) struct NotificationSurface {
|
|
pub id: window::Id,
|
|
output: wl_output::WlOutput,
|
|
height: u32,
|
|
pub effects: Option<EffectsBinding>,
|
|
pub regions: motion::Regions,
|
|
}
|
|
|
|
impl FereseShell {
|
|
pub(super) fn toggle_notification_history(&mut self) -> Task<Message> {
|
|
if self.notifications.history_open {
|
|
return self.close_menu();
|
|
}
|
|
// Cards can open history too. Anchor those requests to the active bar;
|
|
// bell clicks pass their exact anchor through the normal menu path.
|
|
if let Some(output) = self.outputs.iter().find(|entry| {
|
|
self.snapshot
|
|
.outputs
|
|
.iter()
|
|
.any(|output| output.focused && Some(output.name.as_str()) == entry.name.as_deref())
|
|
}) {
|
|
self.bar_surface_id = output.bar;
|
|
}
|
|
let width = self
|
|
.outputs
|
|
.iter()
|
|
.find(|entry| entry.bar == self.bar_surface_id)
|
|
.and_then(|entry| entry.size)
|
|
.map_or(POPUP_WIDTH as i32, |size| size.0);
|
|
self.open_menu(
|
|
status_ui::Menu::Notifications,
|
|
cosmic::iced::Rectangle {
|
|
x: (width - 48).max(0),
|
|
y: 0,
|
|
width: 24,
|
|
height: self.config.theme.bar_height.round() as i32,
|
|
},
|
|
)
|
|
}
|
|
|
|
pub(super) fn close_notification_history(&mut self) -> Task<Message> {
|
|
self.close_menu()
|
|
}
|
|
|
|
pub(super) fn sync_notification_surface(&mut self) -> Task<Message> {
|
|
if self.notifications.ready {
|
|
self.status.notifications = Some(status::Notifications {
|
|
count: self.notifications.unread(),
|
|
dnd: self.notifications.dnd,
|
|
});
|
|
}
|
|
let count = self.notifications.popup_groups().len();
|
|
let wanted = self.notifications.ready && !self.notifications.history_open && count > 0;
|
|
let selected = self
|
|
.outputs
|
|
.iter()
|
|
.find(|entry| {
|
|
self.snapshot.outputs.iter().any(|output| {
|
|
output.focused && Some(output.name.as_str()) == entry.name.as_deref()
|
|
})
|
|
})
|
|
.or_else(|| self.outputs.first())
|
|
.map(|entry| entry.output.clone());
|
|
let mut tasks = Vec::new();
|
|
if self
|
|
.notification_surface
|
|
.as_ref()
|
|
.is_some_and(|surface| !wanted || selected.as_ref() != Some(&surface.output))
|
|
{
|
|
let surface = self.notification_surface.take().unwrap();
|
|
tasks.push(destroy_layer_surface(surface.id));
|
|
}
|
|
if !wanted {
|
|
return Task::batch(tasks);
|
|
}
|
|
let Some(output) = selected else {
|
|
return Task::batch(tasks);
|
|
};
|
|
let desired_height = self
|
|
.notifications
|
|
.popup_groups()
|
|
.iter()
|
|
.map(|(notice, _, count)| {
|
|
popup_height(notice, *count) + if *count > 1 { 12 } else { 0 }
|
|
})
|
|
.sum::<u32>()
|
|
+ count.saturating_sub(1) as u32 * 8
|
|
+ 8;
|
|
let output_height = self
|
|
.outputs
|
|
.iter()
|
|
.find(|entry| entry.output == output)
|
|
.and_then(|entry| entry.size)
|
|
.map_or(1080, |size| size.1);
|
|
let top = self.config.theme.bar_height + self.config.theme.bar_margin_top as f32 + 12.0;
|
|
let height = desired_height.min((output_height as f32 - top - 12.0).max(100.0) as u32);
|
|
if let Some(surface) = &mut self.notification_surface {
|
|
if surface.height != height {
|
|
surface.height = height;
|
|
tasks.push(set_size(surface.id, Some(POPUP_WIDTH), Some(height)));
|
|
}
|
|
} else {
|
|
let id = window::Id::unique();
|
|
self.notification_surface = Some(NotificationSurface {
|
|
id,
|
|
output: output.clone(),
|
|
height,
|
|
effects: None,
|
|
regions: Default::default(),
|
|
});
|
|
let top = self.config.theme.bar_height + self.config.theme.bar_margin_top as f32 + 12.0;
|
|
let action = cosmic::surface::action::app_layer_shell::<Self>(
|
|
|_| Default::default(),
|
|
move |_| SctkLayerSurfaceSettings {
|
|
id,
|
|
layer: Layer::Overlay,
|
|
keyboard_interactivity: KeyboardInteractivity::None,
|
|
anchor: Anchor::TOP | Anchor::RIGHT,
|
|
output: IcedOutput::Output(output.clone()),
|
|
margin: IcedMargin {
|
|
top: top as i32,
|
|
right: 12,
|
|
..Default::default()
|
|
},
|
|
exclusive_zone: -1,
|
|
size: Some((Some(POPUP_WIDTH), Some(height))),
|
|
size_limits: Limits::NONE,
|
|
namespace: "ferese-shell-notifications".into(),
|
|
..Default::default()
|
|
},
|
|
Some(Box::new(Self::view_notifications)),
|
|
);
|
|
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
|
|
}
|
|
Task::batch(tasks)
|
|
}
|
|
|
|
fn notification_card(
|
|
&self,
|
|
notice: ¬ifications::Notice,
|
|
opacity: f32,
|
|
history: bool,
|
|
count: usize,
|
|
) -> Element<'_, cosmic::Action<Message>> {
|
|
let palette = self.config.theme;
|
|
let tint = move |rgba| {
|
|
let mut value = color(rgba);
|
|
value.a *= opacity;
|
|
value
|
|
};
|
|
let foreground = tint(palette.text_primary);
|
|
let muted = tint(palette.text_muted);
|
|
let accent = tint(palette.accent);
|
|
let base = tint(if history {
|
|
palette.surface_base
|
|
} else {
|
|
palette.surface_popover
|
|
});
|
|
let compositor_material = !history
|
|
&& self
|
|
.notification_surface
|
|
.as_ref()
|
|
.is_some_and(|surface| surface.effects.is_some());
|
|
let divider = tint(palette.border);
|
|
let well = blend(base, foreground, 0.07);
|
|
let id = notice.id;
|
|
let mut bold = *SHELL_FONT.get().unwrap().read().unwrap();
|
|
bold.weight = cosmic::iced::font::Weight::Bold;
|
|
let app_icon = if notice.app.to_lowercase().starts_with("ferese") || notice.icon.is_empty()
|
|
{
|
|
accented_icon(ferese_theme::icons::FERESE, 18, accent, accent)
|
|
} else if notice.icon.starts_with('/') {
|
|
icon::icon(icon::from_path(notice.icon.clone().into())).size(18)
|
|
} else {
|
|
icon::from_name(notice.icon.as_str()).size(18).icon()
|
|
}
|
|
.opacity(opacity);
|
|
let icon_well =
|
|
container(app_icon)
|
|
.center_x(30)
|
|
.center_y(30)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: Some(Background::Color(well)),
|
|
border: Border {
|
|
radius: motion::radius(15.0).into(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
}));
|
|
let icon: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
|
let badge_fill = ferese_theme::composite(
|
|
color(palette.accent),
|
|
color(if history {
|
|
palette.surface_base
|
|
} else {
|
|
palette.surface_popover
|
|
}),
|
|
);
|
|
let (badge_fill, badge_text) =
|
|
ferese_theme::accent_pair(badge_fill, color(palette.text_primary));
|
|
let badge = container(
|
|
text(count.to_string())
|
|
.size(10)
|
|
.font(bold)
|
|
.class(theme::Text::Color(badge_text.scale_alpha(opacity))),
|
|
)
|
|
.center_x(16)
|
|
.center_y(16)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: Some(Background::Color(badge_fill.scale_alpha(opacity))),
|
|
border: Border {
|
|
radius: motion::radius(8.0).into(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
}));
|
|
cosmic::iced::widget::stack([
|
|
container(icon_well)
|
|
.padding(cosmic::iced::Padding {
|
|
top: 4.0,
|
|
..Default::default()
|
|
})
|
|
.into(),
|
|
container(badge).align_right(34).into(),
|
|
])
|
|
.into()
|
|
} else {
|
|
icon_well.into()
|
|
};
|
|
let app = if notice.app.is_empty() {
|
|
"NOTIFICATION".to_owned()
|
|
} else {
|
|
notice.app.to_uppercase()
|
|
};
|
|
let app = if count > 1 {
|
|
format!("{app} · {count} NOTIFICATIONS")
|
|
} else {
|
|
app
|
|
};
|
|
let header = row([])
|
|
.spacing(10)
|
|
.align_y(alignment::Vertical::Center)
|
|
.push(icon)
|
|
.push(
|
|
container(
|
|
text(app)
|
|
.size(11)
|
|
.font(bold)
|
|
.class(theme::Text::Color(muted)),
|
|
)
|
|
.width(Length::Fill)
|
|
.height(16)
|
|
.clip(true),
|
|
)
|
|
.push(
|
|
text(age_label(notice.received_at.elapsed()))
|
|
.size(11)
|
|
.class(theme::Text::Color(muted)),
|
|
)
|
|
.push(icon_control(
|
|
ferese_theme::icons::CLOSE,
|
|
"Dismiss notification",
|
|
if history && count > 1 {
|
|
Message::RemoveNotificationGroup(notice.app.clone())
|
|
} else if history {
|
|
Message::RemoveNotification(id)
|
|
} else {
|
|
Message::DismissNotification(id)
|
|
},
|
|
muted,
|
|
well,
|
|
22,
|
|
));
|
|
let mut content = column([]).spacing(4).push(header).push(
|
|
container(
|
|
text(notice.title.clone())
|
|
.size(14)
|
|
.font(bold)
|
|
.class(theme::Text::Color(foreground)),
|
|
)
|
|
.height(18)
|
|
.clip(true),
|
|
);
|
|
let body = if count > 1 {
|
|
if notice.body.is_empty() {
|
|
format!("{} more", count - 1)
|
|
} else {
|
|
format!("{} · {} more", notice.body, count - 1)
|
|
}
|
|
} else {
|
|
notice.body.clone()
|
|
};
|
|
if !body.is_empty() {
|
|
content = content.push(
|
|
container(text(body).size(13).class(theme::Text::Color(muted)))
|
|
.height(if history && count == 1 {
|
|
Length::Shrink
|
|
} else {
|
|
Length::Fixed(18.0)
|
|
})
|
|
.clip(true),
|
|
);
|
|
}
|
|
let mut face = column([]).push(container(content).padding([12, 14]).width(Length::Fill));
|
|
if notice.live {
|
|
let available: Vec<_> = notice
|
|
.actions
|
|
.iter()
|
|
.filter(|(key, _)| key != "default")
|
|
.take(2)
|
|
.collect();
|
|
if !available.is_empty() {
|
|
let mut actions = row([]);
|
|
for (index, (key, label)) in available.into_iter().enumerate() {
|
|
if index > 0 {
|
|
actions = actions.push(
|
|
container(cosmic::iced::widget::Space::new().width(1).height(38))
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: Some(Background::Color(divider)),
|
|
..Default::default()
|
|
})),
|
|
);
|
|
}
|
|
actions = actions.push(card_button(
|
|
container(
|
|
text(label.clone())
|
|
.font(bold)
|
|
.size(13)
|
|
.class(theme::Text::Color(accent)),
|
|
)
|
|
.center_x(Length::Fill)
|
|
.center_y(38),
|
|
Message::InvokeNotification(id, key.clone()),
|
|
accent,
|
|
well,
|
|
false,
|
|
label.clone(),
|
|
));
|
|
}
|
|
face = face
|
|
.push(
|
|
container(
|
|
cosmic::iced::widget::Space::new()
|
|
.width(Length::Fill)
|
|
.height(1),
|
|
)
|
|
.class(theme::Container::custom(move |_| {
|
|
container::Style {
|
|
background: Some(Background::Color(divider)),
|
|
..Default::default()
|
|
}
|
|
})),
|
|
)
|
|
.push(actions);
|
|
}
|
|
}
|
|
let card = container(face)
|
|
.id(if history {
|
|
"ferese-history-card"
|
|
} else {
|
|
"ferese-blur-card"
|
|
})
|
|
.width(Length::Fill)
|
|
.clip(true)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: if history {
|
|
Some(Background::Color(Color {
|
|
a: 0.045 * opacity,
|
|
..foreground
|
|
}))
|
|
} else {
|
|
(!compositor_material).then_some(Background::Color(base))
|
|
},
|
|
border: Border {
|
|
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
|
width: if history { 0.0 } else { 1.0 },
|
|
radius: palette.material_radius.into(),
|
|
},
|
|
..Default::default()
|
|
}));
|
|
let card: Element<'_, cosmic::Action<Message>> = card.into();
|
|
let card: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
|
// With translucent materials, complete backing rectangles show
|
|
// through the face and multiply its tint. Paint only the exposed
|
|
// bottom edges, after the card's measured height.
|
|
let back = move |inset: f32| {
|
|
container(
|
|
container(
|
|
cosmic::iced::widget::Space::new()
|
|
.width(Length::Fill)
|
|
.height(6),
|
|
)
|
|
.width(Length::Fill)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: Some(Background::Color(Color {
|
|
a: 0.045 * opacity,
|
|
..foreground
|
|
})),
|
|
border: Border {
|
|
radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
})),
|
|
)
|
|
.padding([0.0, inset])
|
|
};
|
|
column([card, back(6.0).into(), back(12.0).into()]).into()
|
|
} else {
|
|
card
|
|
};
|
|
let mut area = cosmic::widget::mouse_area(card)
|
|
.on_enter(cosmic::Action::App(Message::HoverNotification(id, true)))
|
|
.on_exit(cosmic::Action::App(Message::HoverNotification(id, false)));
|
|
if count > 1 {
|
|
area = area.on_press(cosmic::Action::App(if history {
|
|
Message::ToggleNotificationGroup(notice.app.clone())
|
|
} else {
|
|
Message::ToggleNotificationHistory
|
|
}));
|
|
} else if notice.live && notice.actions.iter().any(|(key, _)| key == "default") {
|
|
area = area.on_press(cosmic::Action::App(Message::InvokeNotification(
|
|
id,
|
|
"default".into(),
|
|
)));
|
|
}
|
|
area.into()
|
|
}
|
|
|
|
pub(super) fn view_notifications(&self) -> Element<'_, cosmic::Action<Message>> {
|
|
if self.notifications.history_open {
|
|
let palette = self.config.theme;
|
|
let surface = self.menu.as_ref();
|
|
let progress = surface.map_or(1.0, status_ui::OpenMenu::progress);
|
|
let opacity = if surface
|
|
.and_then(|surface| surface.effects.as_ref())
|
|
.is_some_and(|effects| effects.surface.version() >= 3)
|
|
{
|
|
1.0
|
|
} else {
|
|
progress
|
|
};
|
|
let color = move |rgba| color_with_opacity(rgba, opacity);
|
|
let primary = color(palette.text_primary);
|
|
let muted = color(palette.text_muted);
|
|
let accent = color(palette.accent);
|
|
let base = color(palette.surface_base);
|
|
let compositor_material = surface.is_some_and(|menu| menu.effects.is_some());
|
|
let well = blend(base, primary, 0.07);
|
|
let mut bold = *SHELL_FONT.get().unwrap().read().unwrap();
|
|
bold.weight = cosmic::iced::font::Weight::Bold;
|
|
let groups = self.notifications.history_groups();
|
|
let count = self.notifications.entries.len();
|
|
let subtitle = if count == 0 {
|
|
"A quiet moment".to_owned()
|
|
} else {
|
|
format!(
|
|
"{count} {} · {} {}",
|
|
if count == 1 {
|
|
"notification"
|
|
} else {
|
|
"notifications"
|
|
},
|
|
groups.len(),
|
|
if groups.len() == 1 { "app" } else { "apps" }
|
|
)
|
|
};
|
|
let heading = column([])
|
|
.spacing(3)
|
|
.push(
|
|
text("Notifications")
|
|
.size(16)
|
|
.font(bold)
|
|
.class(theme::Text::Color(primary)),
|
|
)
|
|
.push(text(subtitle).size(11).class(theme::Text::Color(muted)));
|
|
let header = row([])
|
|
.spacing(10)
|
|
.align_y(alignment::Vertical::Center)
|
|
.push(container(heading).width(Length::Fill))
|
|
.push(icon_control(
|
|
ferese_theme::icons::CLOSE,
|
|
"Close notification center",
|
|
Message::ToggleNotificationHistory,
|
|
muted,
|
|
well,
|
|
28,
|
|
));
|
|
let dnd_color = if self.notifications.dnd {
|
|
accent
|
|
} else {
|
|
muted
|
|
};
|
|
let dnd_icon = if self.notifications.dnd {
|
|
ferese_theme::icons::NOTIFICATIONS_OFF
|
|
} else {
|
|
ferese_theme::icons::NOTIFICATIONS
|
|
};
|
|
let dnd = row([])
|
|
.spacing(10)
|
|
.align_y(alignment::Vertical::Center)
|
|
.push(accented_icon(dnd_icon, 20, dnd_color, dnd_color))
|
|
.push(
|
|
container(
|
|
column([])
|
|
.spacing(2)
|
|
.push(
|
|
text("Do Not Disturb")
|
|
.size(12)
|
|
.font(bold)
|
|
.class(theme::Text::Color(primary)),
|
|
)
|
|
.push(
|
|
text(if self.notifications.dnd {
|
|
"Popups paused"
|
|
} else {
|
|
"Popups enabled"
|
|
})
|
|
.size(11)
|
|
.class(theme::Text::Color(muted)),
|
|
),
|
|
)
|
|
.width(Length::Fill),
|
|
)
|
|
.push(motion::button(
|
|
button::custom(
|
|
container(
|
|
text(if self.notifications.dnd { "On" } else { "Off" })
|
|
.size(12)
|
|
.font(bold)
|
|
.class(theme::Text::Color(dnd_color)),
|
|
)
|
|
.center_x(40)
|
|
.center_y(26),
|
|
)
|
|
.on_press(cosmic::Action::App(Message::Control(status::Action::Dnd(
|
|
!self.notifications.dnd,
|
|
))))
|
|
.padding(0),
|
|
dnd_color,
|
|
self.notifications.dnd,
|
|
1.0,
|
|
));
|
|
let controls =
|
|
container(dnd)
|
|
.padding(10)
|
|
.width(Length::Fill)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: Some(Background::Color(base)),
|
|
border: Border {
|
|
radius: palette.material_radius.into(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
}));
|
|
let mut content = column([]).spacing(10).push(header).push(controls);
|
|
if count == 0 {
|
|
let empty = column([])
|
|
.spacing(10)
|
|
.align_x(alignment::Horizontal::Center)
|
|
.push(
|
|
container(accented_icon(
|
|
ferese_theme::icons::NOTIFICATIONS,
|
|
26,
|
|
muted,
|
|
accent,
|
|
))
|
|
.center_x(56)
|
|
.center_y(56)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: Some(Background::Color(well)),
|
|
border: Border {
|
|
radius: motion::radius(28.0).into(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
})),
|
|
)
|
|
.push(
|
|
text("You're all caught up")
|
|
.size(15)
|
|
.font(bold)
|
|
.class(theme::Text::Color(primary)),
|
|
)
|
|
.push(
|
|
text("New notifications will appear here.")
|
|
.size(11)
|
|
.class(theme::Text::Color(muted)),
|
|
);
|
|
content = content.push(
|
|
container(empty)
|
|
.center_x(Length::Fill)
|
|
.center_y(Length::Fill),
|
|
);
|
|
} else {
|
|
content = content.push(
|
|
row([])
|
|
.align_y(alignment::Vertical::Center)
|
|
.push(
|
|
text("Recent")
|
|
.size(11)
|
|
.font(bold)
|
|
.class(theme::Text::Color(muted))
|
|
.width(Length::Fill),
|
|
)
|
|
.push(icon_control(
|
|
ferese_theme::icons::TRASH,
|
|
"Clear all notifications",
|
|
Message::ClearNotifications,
|
|
muted,
|
|
well,
|
|
26,
|
|
)),
|
|
);
|
|
let mut entries = column([]).spacing(8);
|
|
for group in groups {
|
|
let latest = group[0];
|
|
if group.len() > 1 && self.notifications.expanded_apps.contains(&latest.app) {
|
|
let app = if latest.app.is_empty() {
|
|
"Notifications"
|
|
} else {
|
|
&latest.app
|
|
};
|
|
let group_header = row([])
|
|
.spacing(6)
|
|
.align_y(alignment::Vertical::Center)
|
|
.push(
|
|
text(format!("{} · {}", app, group.len()))
|
|
.size(11)
|
|
.font(bold)
|
|
.class(theme::Text::Color(muted))
|
|
.width(Length::Fill),
|
|
)
|
|
.push(icon_control(
|
|
ferese_theme::icons::CHEVRON_UP,
|
|
"Collapse group",
|
|
Message::ToggleNotificationGroup(latest.app.clone()),
|
|
muted,
|
|
well,
|
|
24,
|
|
))
|
|
.push(icon_control(
|
|
ferese_theme::icons::TRASH,
|
|
"Clear group",
|
|
Message::RemoveNotificationGroup(latest.app.clone()),
|
|
muted,
|
|
well,
|
|
24,
|
|
));
|
|
let mut cards = column([]).spacing(8).push(group_header);
|
|
for notice in group {
|
|
cards = cards.push(self.notification_card(notice, opacity, true, 1));
|
|
}
|
|
entries = entries.push(cards);
|
|
} else {
|
|
entries = entries.push(self.notification_card(
|
|
latest,
|
|
opacity,
|
|
true,
|
|
group.len(),
|
|
));
|
|
}
|
|
}
|
|
content = content.push(
|
|
scrollable(entries)
|
|
.direction(Direction::Vertical(Scrollbar::hidden()))
|
|
.height(Length::Fill),
|
|
);
|
|
}
|
|
let panel = container(content)
|
|
.id("ferese-blur-card")
|
|
.padding(16)
|
|
.height(history_height(&self.notifications) as f32)
|
|
.width(Length::Fill)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: (!compositor_material)
|
|
.then_some(Background::Color(color(palette.surface_popover))),
|
|
border: Border {
|
|
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
|
width: 1.0,
|
|
radius: palette.material_radius.into(),
|
|
},
|
|
..Default::default()
|
|
}))
|
|
.into();
|
|
motion::animated(
|
|
panel,
|
|
progress,
|
|
surface
|
|
.map(|surface| surface.regions.clone())
|
|
.unwrap_or_default(),
|
|
palette.material_radius,
|
|
)
|
|
} else {
|
|
let mut cards = column([]).spacing(8);
|
|
for (notice, opacity, count) in self.notifications.popup_groups() {
|
|
cards = cards.push(self.notification_card(notice, opacity, false, count));
|
|
}
|
|
motion::animated(
|
|
container(
|
|
scrollable(cards)
|
|
.direction(Direction::Vertical(Scrollbar::hidden()))
|
|
.height(Length::Fill),
|
|
)
|
|
.padding(4)
|
|
.into(),
|
|
1.0,
|
|
self.notification_surface
|
|
.as_ref()
|
|
.map(|surface| surface.regions.clone())
|
|
.unwrap_or_default(),
|
|
self.config.theme.material_radius,
|
|
)
|
|
}
|
|
}
|
|
}
|