feat: libcosmic iced 0.14 rebase

This commit is contained in:
Ashley Wulber 2026-03-17 16:56:55 -04:00 committed by GitHub
parent 0020132e63
commit cf7fc32adf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2731 additions and 1869 deletions

2636
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,14 @@ cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" }
tokio = { version = "1.49.0", features = ["macros"] } tokio = { version = "1.49.0", features = ["macros"] }
[workspace.dependencies.libcosmic] [workspace.dependencies.libcosmic]
features = ["dbus-config", "desktop", "multi-window", "winit", "tokio", "qr_code"] features = [
"dbus-config",
"desktop",
"multi-window",
"winit",
"tokio",
"qr_code",
]
git = "https://github.com/pop-os/libcosmic" git = "https://github.com/pop-os/libcosmic"
[workspace.dependencies.cosmic-config] [workspace.dependencies.cosmic-config]
@ -54,9 +61,9 @@ debug = true
# [patch.'https://github.com/pop-os/cosmic-text'] # [patch.'https://github.com/pop-os/cosmic-text']
# cosmic-text = { git = "https://github.com/pop-os/cosmic-text//", rev = "b017d7c" } # cosmic-text = { git = "https://github.com/pop-os/cosmic-text//", rev = "b017d7c" }
# [patch.'https://github.com/pop-os/cosmic-protocols'] [patch.'https://github.com/pop-os/cosmic-protocols']
# cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", rev = "d0e95be" } cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", rev = "d0e95be" }
# cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", rev = "d0e95be" } cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", rev = "d0e95be" }
# [patch.'https://github.com/pop-os/cosmic-settings-daemon'] # [patch.'https://github.com/pop-os/cosmic-settings-daemon']
# cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon//", branch = "input_nobuild" } # cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon//", branch = "input_nobuild" }
@ -67,8 +74,17 @@ debug = true
# cosmic-config = { path = "../libcosmic/cosmic-config" } # cosmic-config = { path = "../libcosmic/cosmic-config" }
# cosmic-theme = { path = "../libcosmic/cosmic-theme" } # cosmic-theme = { path = "../libcosmic/cosmic-theme" }
# iced_futures = { path = "../libcosmic/iced/futures" } # iced_futures = { path = "../libcosmic/iced/futures" }
#
#iced_futures = { git = "https://github.com/pop-os/libcosmic//" }
#libcosmic = { git = "https://github.com/pop-os/libcosmic//" }
#cosmic-config = { git = "https://github.com/pop-os/libcosmic//" }
#cosmic-theme = { git = "https://github.com/pop-os/libcosmic//" }
# [patch.'https://github.com/pop-os/dbus-settings-bindings'] # [patch.'https://github.com/pop-os/dbus-settings-bindings']
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" } # cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }
# upower_dbus = { path = "../dbus-settings-bindings/upower" } # upower_dbus = { path = "../dbus-settings-bindings/upower" }
# nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings//", branch = "nm-secret-agent" } # nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings//", branch = "nm-secret-agent" }
[patch.crates-io]
atspi = { git = "https://github.com/wash2/atspi" }
atspi-common = { git = "https://github.com/wash2/atspi" }

View file

@ -61,7 +61,7 @@ itertools = "0.14.0"
itoa = "1.0.17" itoa = "1.0.17"
libcosmic.workspace = true libcosmic.workspace = true
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
sysinfo = { version = "0.38.2", optional = true } sysinfo = { version = "=0.38.0", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", optional = true } mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", optional = true }
notify = "8.2.0" notify = "8.2.0"
regex = "1.12.3" regex = "1.12.3"
@ -173,7 +173,13 @@ page-networking = [
"dep:zbus", "dep:zbus",
] ]
page-power = ["dep:upower_dbus", "dep:zbus"] page-power = ["dep:upower_dbus", "dep:zbus"]
page-region = ["gettext", "dep:locales-rs", "dep:locale1", "dep:zbus", "dep:accounts-zbus"] page-region = [
"gettext",
"dep:locales-rs",
"dep:locale1",
"dep:zbus",
"dep:accounts-zbus",
]
page-sound = ["dep:cosmic-settings-sound-subscription"] page-sound = ["dep:cosmic-settings-sound-subscription"]
page-users = ["xdg-portal", "dep:accounts-zbus", "dep:zbus", "dep:zbus_polkit"] page-users = ["xdg-portal", "dep:accounts-zbus", "dep:zbus", "dep:zbus_polkit"]
page-window-management = ["cosmic-comp-config", "dep:cosmic-settings-config"] page-window-management = ["cosmic-comp-config", "dep:cosmic-settings-config"]
@ -186,10 +192,6 @@ cosmic-comp-config = ["dep:cosmic-comp-config"]
dbus-config = ["libcosmic/dbus-config", "cosmic-config/dbus"] dbus-config = ["libcosmic/dbus-config", "cosmic-config/dbus"]
single-instance = ["libcosmic/single-instance"] single-instance = ["libcosmic/single-instance"]
test = [] test = []
wayland = [ wayland = ["libcosmic/wayland", "dep:cosmic-panel-config", "dep:cosmic-randr"]
"libcosmic/wayland",
"dep:cosmic-panel-config",
"dep:cosmic-randr"
]
wgpu = ["libcosmic/wgpu"] wgpu = ["libcosmic/wgpu"]
xdg-portal = ["ashpd", "libcosmic/xdg-portal"] xdg-portal = ["ashpd", "libcosmic/xdg-portal"]

View file

@ -131,7 +131,7 @@ impl page::Page<crate::pages::Message> for Page {
return cosmic::Task::stream(cosmic::iced_futures::stream::channel( return cosmic::Task::stream(cosmic::iced_futures::stream::channel(
1, 1,
|mut sender| async move { |mut sender: futures::channel::mpsc::Sender<crate::pages::Message>| async move {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
let _ = sender let _ = sender
.send(crate::pages::Message::AccessibilityMagnifier( .send(crate::pages::Message::AccessibilityMagnifier(

View file

@ -129,7 +129,7 @@ impl page::Page<crate::pages::Message> for Page {
return cosmic::Task::stream(cosmic::iced_futures::stream::channel( return cosmic::Task::stream(cosmic::iced_futures::stream::channel(
1, 1,
|mut sender| async move { |mut sender: futures::channel::mpsc::Sender<super::Message>| async move {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
let _ = sender let _ = sender
.send(crate::pages::Message::Accessibility(Message::Event( .send(crate::pages::Message::Accessibility(Message::Event(

View file

@ -10,7 +10,9 @@ use std::{
}; };
use cosmic::{ use cosmic::{
Apply, Element, Task, surface, Apply, Element, Task,
iced::Alignment,
surface,
widget::{self, dropdown, icon, settings}, widget::{self, dropdown, icon, settings},
}; };
use cosmic_config::{ConfigGet, ConfigSet}; use cosmic_config::{ConfigGet, ConfigSet};
@ -296,6 +298,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "web-browser"), fl!("default-apps", "web-browser"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "web-browser"), fl!("default-apps", "web-browser"),
@ -313,6 +316,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
.min_item_width(300.0) .min_item_width(300.0)
} }
}) })
@ -323,6 +327,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "file-manager"), fl!("default-apps", "file-manager"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "file-manager"), fl!("default-apps", "file-manager"),
@ -340,6 +345,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
} }
}) })
.add({ .add({
@ -349,6 +355,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "mail-client"), fl!("default-apps", "mail-client"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "mail-client"), fl!("default-apps", "mail-client"),
@ -366,6 +373,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
} }
}) })
.add({ .add({
@ -375,6 +383,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "music"), fl!("default-apps", "music"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "music"), fl!("default-apps", "music"),
@ -392,6 +401,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
} }
}) })
.add({ .add({
@ -401,6 +411,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "video"), fl!("default-apps", "video"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "video"), fl!("default-apps", "video"),
@ -418,6 +429,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
} }
}) })
.add({ .add({
@ -427,6 +439,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "photos"), fl!("default-apps", "photos"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "photos"), fl!("default-apps", "photos"),
@ -444,6 +457,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
} }
}) })
.add({ .add({
@ -453,6 +467,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "calendar"), fl!("default-apps", "calendar"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "calendar"), fl!("default-apps", "calendar"),
@ -470,6 +485,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
} }
}) })
.add({ .add({
@ -479,6 +495,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "terminal"), fl!("default-apps", "terminal"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "terminal"), fl!("default-apps", "terminal"),
@ -496,6 +513,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
} }
}) })
.add({ .add({
@ -505,6 +523,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "text-editor"), fl!("default-apps", "text-editor"),
widget::text(fl!("default-apps", "not-installed")), widget::text(fl!("default-apps", "not-installed")),
) )
.align_items(Alignment::Center)
} else { } else {
settings::flex_item( settings::flex_item(
fl!("default-apps", "text-editor"), fl!("default-apps", "text-editor"),
@ -522,6 +541,7 @@ fn apps() -> Section<crate::pages::Message> {
) )
.icons(Cow::Borrowed(&meta.icons)), .icons(Cow::Borrowed(&meta.icons)),
) )
.align_items(Alignment::Center)
} }
}) })
.apply(Element::from) .apply(Element::from)

View file

@ -137,7 +137,7 @@ impl page::Page<crate::pages::Message> for Page {
// Forward messages from another thread to prevent the monitoring thread from blocking. // Forward messages from another thread to prevent the monitoring thread from blocking.
let (randr_task, randr_handle) = Task::stream(cosmic::iced_futures::stream::channel( let (randr_task, randr_handle) = Task::stream(cosmic::iced_futures::stream::channel(
1, 1,
|mut sender| async move { |mut sender: futures::channel::mpsc::Sender<_>| async move {
while let Some(message) = rx.recv().await { while let Some(message) = rx.recv().await {
if let cosmic_randr::Message::ManagerDone = message if let cosmic_randr::Message::ManagerDone = message
&& !refresh_pending.swap(true, Ordering::SeqCst) && !refresh_pending.swap(true, Ordering::SeqCst)

View file

@ -3,7 +3,7 @@
use cosmic::iced::{Alignment, Length, color}; use cosmic::iced::{Alignment, Length, color};
use cosmic::iced_core::text::Wrapping; use cosmic::iced_core::text::Wrapping;
use cosmic::widget::{self, settings, text}; use cosmic::widget::{self, settings, space::horizontal as horizontal_space, text};
use cosmic::{Apply, Element, Task, theme}; use cosmic::{Apply, Element, Task, theme};
use cosmic_settings_bluetooth_subscription::*; use cosmic_settings_bluetooth_subscription::*;
use cosmic_settings_page::{self as page, Section, section}; use cosmic_settings_page::{self as page, Section, section};
@ -878,7 +878,7 @@ fn connected_devices() -> Section<crate::pages::Message> {
.wrapping(Wrapping::Word) .wrapping(Wrapping::Word)
.into() .into()
}, },
widget::horizontal_space().into(), horizontal_space().into(),
match device.enabled { match device.enabled {
Active::Enabled => widget::text(&descriptions[device_connected]).into(), Active::Enabled => widget::text(&descriptions[device_connected]).into(),
Active::Enabling => widget::text(&descriptions[device_connecting]) Active::Enabling => widget::text(&descriptions[device_connecting])
@ -936,18 +936,26 @@ fn available_devices() -> Section<crate::pages::Message> {
let mut items = vec![ let mut items = vec![
widget::icon::from_name(device.icon).size(16).into(), widget::icon::from_name(device.icon).size(16).into(),
text(device.alias_or_addr()).wrapping(Wrapping::Word).into(), text(device.alias_or_addr()).wrapping(Wrapping::Word).into(),
widget::horizontal_space().into(), horizontal_space().into(),
]; ];
if device.enabled == Active::Disabled { if device.enabled == Active::Disabled {
items.push(widget::button::text(&descriptions[device_connect]).on_press(Message::ConnectDevice(path.clone())).into(), ) items.push(
widget::button::text(&descriptions[device_connect])
.on_press(Message::ConnectDevice(path.clone()))
.into(),
)
} }
if device.enabled == Active::Enabling || device.enabled == Active::Enabled { if device.enabled == Active::Enabling || device.enabled == Active::Enabled {
items.push(text(&descriptions[device_connecting]).class(theme::Text::Color(color!(128, 128, 128))).into(), ); items.push(
text(&descriptions[device_connecting])
.class(theme::Text::Color(color!(128, 128, 128)))
.into(),
);
} }
Some(widget::mouse_area(settings::item_row(items)).into(), ) Some(widget::mouse_area(settings::item_row(items)).into())
}) })
.fold(section, settings::Section::add) .fold(section, settings::Section::add)
.apply(Element::from) .apply(Element::from)
@ -978,11 +986,9 @@ fn multiple_adapter() -> Section<crate::pages::Message> {
widget::icon::from_name("bluetooth-symbolic") widget::icon::from_name("bluetooth-symbolic")
.size(20) .size(20)
.into(), .into(),
widget::horizontal_space() horizontal_space().width(theme::spacing().space_xxs).into(),
.width(theme::spacing().space_xxs)
.into(),
text(&adapter.alias).wrapping(Wrapping::Word).into(), text(&adapter.alias).wrapping(Wrapping::Word).into(),
widget::horizontal_space().into(), horizontal_space().into(),
widget::icon::from_name("go-next-symbolic").into(), widget::icon::from_name("go-next-symbolic").into(),
]; ];
if page.model.adapter_connected(path) { if page.model.adapter_connected(path) {

View file

@ -8,7 +8,7 @@ use cosmic::{
Apply, Element, Task, Apply, Element, Task,
config::{CosmicTk, FontConfig}, config::{CosmicTk, FontConfig},
iced_core::text::Wrapping, iced_core::text::Wrapping,
widget::{self, settings, svg}, widget::{self, settings, space::horizontal as horizontal_space, svg},
}; };
use cosmic_config::ConfigSet; use cosmic_config::ConfigSet;
@ -213,7 +213,7 @@ impl Model {
.class(cosmic::theme::Svg::Custom(svg_accent.clone())) .class(cosmic::theme::Svg::Custom(svg_accent.clone()))
.into() .into()
} else { } else {
widget::horizontal_space().width(16).into() horizontal_space().width(16.).into()
}, },
]) ])
.apply(widget::container) .apply(widget::container)

View file

@ -22,8 +22,8 @@ use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced::Subscription; use cosmic::iced::Subscription;
use cosmic::iced_core::{Alignment, Length}; use cosmic::iced_core::{Alignment, Length};
use cosmic::widget::{ use cosmic::widget::{
button, color_picker::ColorPickerUpdate, container, horizontal_space, radio, row, settings, button, color_picker::ColorPickerUpdate, container, radio, row, settings,
text, space::horizontal as horizontal_space, text,
}; };
use cosmic::{Apply, Element, Task, widget}; use cosmic::{Apply, Element, Task, widget};
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
@ -926,7 +926,7 @@ pub fn reset_button() -> Section<crate::pages::Message> {
.on_press(Message::Reset) .on_press(Message::Reset)
.into() .into()
} else { } else {
horizontal_space().width(1).apply(Element::from) horizontal_space().width(1.).apply(Element::from)
} }
.map(crate::pages::Message::Appearance) .map(crate::pages::Message::Appearance)
}) })

View file

@ -899,30 +899,28 @@ where
} }
fn layout( fn layout(
&self, &mut self,
tree: &mut Tree, tree: &mut Tree,
renderer: &cosmic::Renderer, renderer: &cosmic::Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
let inner_layout = self let inner_layout =
.inner self.inner
.as_widget() .as_widget_mut()
.layout(&mut tree.children[0], renderer, limits); .layout(&mut tree.children[0], renderer, limits);
layout::Node::with_children(inner_layout.size(), vec![inner_layout]) layout::Node::with_children(inner_layout.size(), vec![inner_layout])
} }
fn operate( fn operate(
&self, &mut self,
tree: &mut Tree, tree: &mut Tree,
layout: layout::Layout<'_>, layout: layout::Layout<'_>,
renderer: &cosmic::Renderer, renderer: &cosmic::Renderer,
operation: &mut dyn Operation<()>, operation: &mut dyn Operation<()>,
) { ) {
let state = tree.state.downcast_mut::<ReorderWidgetState>(); operation.container(Some(&self.id), layout.bounds());
operation.custom(state, Some(&self.id)); self.inner.as_widget_mut().operate(
self.inner.as_widget().operate(
&mut tree.children[0], &mut tree.children[0],
layout.children().next().unwrap(), layout.children().next().unwrap(),
renderer, renderer,
@ -931,31 +929,31 @@ where
} }
#[allow(clippy::too_many_lines, clippy::needless_match)] #[allow(clippy::too_many_lines, clippy::needless_match)]
fn on_event( fn update(
&mut self, &mut self,
tree: &mut Tree, tree: &mut Tree,
event: event::Event, event: &event::Event,
layout: layout::Layout<'_>, layout: layout::Layout<'_>,
cursor_position: mouse::Cursor, cursor_position: mouse::Cursor,
renderer: &cosmic::Renderer, renderer: &cosmic::Renderer,
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) -> event::Status { ) {
let space_xxs = theme::spacing().space_xxs; let space_xxs = theme::spacing().space_xxs;
let mut ret = match self.inner.as_widget_mut().on_event( self.inner.as_widget_mut().update(
&mut tree.children[0], &mut tree.children[0],
event.clone(), event,
layout.children().next().unwrap(), layout.children().next().unwrap(),
cursor_position, cursor_position,
renderer, renderer,
clipboard, clipboard,
shell, shell,
viewport, viewport,
) { );
event::Status::Captured => return event::Status::Captured, if shell.is_event_captured() {
event::Status::Ignored => event::Status::Ignored, return;
}; }
let height = (layout.bounds().height let height = (layout.bounds().height
- space_xxs as f32 * (self.info.len().saturating_sub(1)) as f32) - space_xxs as f32 * (self.info.len().saturating_sub(1)) as f32)
@ -967,15 +965,14 @@ where
DraggingState::Dragging(applet) => match &event { DraggingState::Dragging(applet) => match &event {
event::Event::Dnd(DndEvent::Source(source_event)) => match source_event { event::Event::Dnd(DndEvent::Source(source_event)) => match source_event {
SourceEvent::Cancelled => { SourceEvent::Cancelled => {
ret = event::Status::Captured; shell.capture_event();
if let Some(on_cancel) = self.on_cancel.clone() { if let Some(on_cancel) = self.on_cancel.clone() {
shell.publish(on_cancel); shell.publish(on_cancel);
} }
DraggingState::None DraggingState::None
} }
SourceEvent::Finished => { SourceEvent::Finished => {
ret = event::Status::Captured; shell.capture_event();
DraggingState::None DraggingState::None
} }
_ => DraggingState::Dragging(applet), _ => DraggingState::Dragging(applet),
@ -989,7 +986,7 @@ where
| event::Event::Touch(touch::Event::FingerPressed { .. }) | event::Event::Touch(touch::Event::FingerPressed { .. })
if cursor_position.is_over(layout.bounds()) => if cursor_position.is_over(layout.bounds()) =>
{ {
ret = event::Status::Captured; shell.capture_event();
DraggingState::Pressed(cursor_position.position().unwrap_or_default()) DraggingState::Pressed(cursor_position.position().unwrap_or_default())
} }
@ -1040,7 +1037,8 @@ where
Box::new(AppletString(p.clone())), Box::new(AppletString(p.clone())),
DndAction::Move, DndAction::Move,
); );
ret = event::Status::Captured; shell.capture_event();
let reordered = self let reordered = self
.info .info
.iter() .iter()
@ -1063,7 +1061,7 @@ where
| event::Event::Touch( | event::Event::Touch(
touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }, touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. },
) => { ) => {
ret = event::Status::Captured; shell.capture_event();
DraggingState::None DraggingState::None
} }
_ => DraggingState::Pressed(start), _ => DraggingState::Pressed(start),
@ -1161,8 +1159,6 @@ where
_ => DndOfferState::HandlingOffer, _ => DndOfferState::HandlingOffer,
}, },
}; };
ret
} }
fn draw( fn draw(
@ -1189,14 +1185,16 @@ where
fn overlay<'b>( fn overlay<'b>(
&'b mut self, &'b mut self,
tree: &'b mut Tree, tree: &'b mut Tree,
layout: layout::Layout<'_>, layout: layout::Layout<'b>,
renderer: &cosmic::Renderer, renderer: &cosmic::Renderer,
viewport: &Rectangle,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, cosmic::Theme, cosmic::Renderer>> { ) -> Option<overlay::Element<'b, Message, cosmic::Theme, cosmic::Renderer>> {
self.inner.as_widget_mut().overlay( self.inner.as_widget_mut().overlay(
&mut tree.children[0], &mut tree.children[0],
layout.children().next().unwrap(), layout.children().next().unwrap(),
renderer, renderer,
viewport,
translation, translation,
) )
} }

View file

@ -6,7 +6,8 @@ use cosmic::{
iced::{Alignment, Length}, iced::{Alignment, Length},
surface, theme, surface, theme,
widget::{ widget::{
button, container, dropdown, horizontal_space, icon, row, settings, slider, text, toggler, button, container, dropdown, icon, row, settings, slider,
space::horizontal as horizontal_space, text, toggler,
}, },
}; };
@ -203,8 +204,7 @@ pub(crate) fn style<
move |a| crate::app::Message::PageMessage(msg_map(a)), move |a| crate::app::Message::PageMessage(msg_map(a)),
), ),
)) ))
.add(settings::flex_item( .add(settings::item::builder(&descriptions[size]).flex_control({
&descriptions[size],
// TODO custom discrete slider variant // TODO custom discrete slider variant
row::with_children(vec![ row::with_children(vec![
text::body(fl!("small")).into(), text::body(fl!("small")).into(),
@ -232,35 +232,43 @@ pub(crate) fn style<
} }
}, },
) )
.width(Length::Fill)
.apply(cosmic::widget::container)
.max_width(250)
.into(), .into(),
text::body(fl!("large")).into(), text::body(fl!("large")).into(),
]) ])
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(8), .spacing(8)
)) .width(Length::Fill)
.add(settings::flex_item( }))
&descriptions[background_opacity], .add(
row::with_capacity(2) settings::item::builder(&descriptions[background_opacity]).flex_control({
.align_y(Alignment::Center) row::with_capacity(2)
.spacing(8) .align_y(Alignment::Center)
.push( .spacing(8)
text::body(fl!( .width(Length::Fill)
"number", .push(
HashMap::from_iter(vec![( text::body(fl!(
"number", "number",
(panel_config.opacity * 100.0) as i32 HashMap::from_iter(vec![(
)]) "number",
)) (panel_config.opacity * 100.0) as i32
.width(Length::Fixed(22.0)) )])
.align_x(Alignment::Center), ))
) .width(Length::Fixed(22.0))
.push( .align_x(Alignment::Center),
slider(0..=100, (panel_config.opacity * 100.0) as i32, |v| { )
Message::OpacityRequest(v as f32 / 100.0) .push(
}) slider(0..=100, (panel_config.opacity * 100.0) as i32, |v| {
.breakpoints(&[50]), Message::OpacityRequest(v as f32 / 100.0)
), })
)) .width(Length::Fill)
.apply(container)
.max_width(250),
)
}),
)
.apply(Element::from) .apply(Element::from)
.map(msg_map) .map(msg_map)
}) })
@ -293,10 +301,13 @@ pub(crate) fn configuration<P: page::Page<crate::pages::Message> + PanelPage>(
settings::item::builder(&*descriptions[applets_label]) settings::item::builder(&*descriptions[applets_label])
.control(control) .control(control)
.spacing(16) .spacing(16)
.width(Length::Fill)
.apply(container) .apply(container)
.class(theme::Container::List) .class(theme::Container::List)
.apply(button::custom) .apply(button::custom)
.width(Length::Fill)
.class(theme::Button::Transparent) .class(theme::Button::Transparent)
.width(Length::Fill)
.on_press(crate::pages::Message::Page(panel_applets_entity)), .on_press(crate::pages::Message::Page(panel_applets_entity)),
) )
} else { } else {
@ -347,7 +358,7 @@ pub fn reset_button<
let descriptions = &section.descriptions; let descriptions = &section.descriptions;
let inner = page.inner(); let inner = page.inner();
if inner.system_default == inner.panel_config { if inner.system_default == inner.panel_config {
Element::from(horizontal_space().width(1)) Element::from(horizontal_space().width(1.))
} else { } else {
button::standard(&descriptions[reset_to_default]) button::standard(&descriptions[reset_to_default])
.on_press(Message::ResetPanel) .on_press(Message::ResetPanel)

View file

@ -25,7 +25,9 @@ use cosmic::{
widget::{ widget::{
button, dropdown, list_column, row, button, dropdown, list_column, row,
segmented_button::{self, SingleSelectModel}, segmented_button::{self, SingleSelectModel},
settings, tab_bar, text, toggler, settings,
space::horizontal as horizontal_space,
tab_bar, text, toggler,
}, },
}; };
use cosmic::{ use cosmic::{
@ -691,7 +693,10 @@ impl Page {
.width(Length::Fixed(SIMULATED_WIDTH as f32)) .width(Length::Fixed(SIMULATED_WIDTH as f32))
.into(), .into(),
None => cosmic::widget::Space::new(SIMULATED_WIDTH, SIMULATED_HEIGHT).into(), None => cosmic::widget::Space::new()
.width(Length::Fixed(SIMULATED_WIDTH as f32))
.height(Length::Fixed(SIMULATED_HEIGHT as f32))
.into(),
} }
} }
@ -1342,7 +1347,7 @@ pub fn settings() -> Section<crate::pages::Message> {
}, },
) )
.push(category_selection) .push(category_selection)
.push(cosmic::widget::horizontal_space()) .push(horizontal_space())
.push_maybe(add_button) .push_maybe(add_button)
.into(), .into(),
); );

View file

@ -47,11 +47,12 @@ pub fn color_image<'a, M: 'a>(
height: u16, height: u16,
border_radius: Option<f32>, border_radius: Option<f32>,
) -> Element<'a, M> { ) -> Element<'a, M> {
container(Space::new(width, height)) container(Space::new().width(width).height(height))
.class(cosmic::theme::Container::custom(move |theme| { .class(cosmic::theme::Container::custom(move |theme| {
container::Style { container::Style {
icon_color: None, icon_color: None,
text_color: None, text_color: None,
snap: true,
background: Some(match &color { background: Some(match &color {
wallpaper::Color::Single([r, g, b]) => { wallpaper::Color::Single([r, g, b]) => {
Background::Color(Color::from_rgb(*r, *g, *b)) Background::Color(Color::from_rgb(*r, *g, *b))

View file

@ -3,7 +3,7 @@
use cosmic::{ use cosmic::{
Apply, Element, Apply, Element,
iced::Length, iced::{Alignment, Length},
surface, surface,
widget::{self, settings, toggler}, widget::{self, settings, toggler},
}; };
@ -258,12 +258,15 @@ pub fn window_management() -> Section<crate::pages::Message> {
}, },
), ),
)) ))
.add(settings::flex_item( .add(
&descriptions[edge_gravity], settings::flex_item(
toggler(page.edge_snap_threshold != 0).on_toggle(|is_enabled| { &descriptions[edge_gravity],
Message::SetEdgeSnapThreshold(if is_enabled { 10 } else { 0 }) toggler(page.edge_snap_threshold != 0).on_toggle(|is_enabled| {
}), Message::SetEdgeSnapThreshold(if is_enabled { 10 } else { 0 })
)) }),
)
.align_items(Alignment::Center),
)
.apply(Element::from) .apply(Element::from)
.map(crate::pages::Message::WindowManagement) .map(crate::pages::Message::WindowManagement)
}) })

View file

@ -9,7 +9,7 @@ use cosmic::iced_core::{
Shell, Size, Widget, Shell, Size, Widget,
}; };
use cosmic::iced_core::{Point, layout, mouse, renderer, touch}; use cosmic::iced_core::{Point, layout, mouse, renderer, touch};
use cosmic::iced_core::{alignment, event, text}; use cosmic::iced_core::{alignment, text};
use cosmic::widget::segmented_button::{self, SingleSelectModel}; use cosmic::widget::segmented_button::{self, SingleSelectModel};
use cosmic_randr_shell::{self as randr, OutputKey}; use cosmic_randr_shell::{self as randr, OutputKey};
use randr::Transform; use randr::Transform;
@ -96,7 +96,7 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
} }
fn layout( fn layout(
&self, &mut self,
tree: &mut Tree, tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
@ -157,17 +157,17 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
layout::Node::new(size) layout::Node::new(size)
} }
fn on_event( fn update(
&mut self, &mut self,
tree: &mut Tree, tree: &mut Tree,
event: cosmic::iced_core::Event, event: &cosmic::iced_core::Event,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
_renderer: &Renderer, _renderer: &Renderer,
_clipboard: &mut dyn Clipboard, _clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) -> event::Status { ) {
let bounds = layout.bounds(); let bounds = layout.bounds();
match event { match event {
@ -198,7 +198,7 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
), ),
); );
return event::Status::Captured; shell.capture_event();
} }
} }
} }
@ -217,7 +217,7 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
state.drag_from = position; state.drag_from = position;
state.offset = (position.x - output_region.x, position.y - output_region.y); state.offset = (position.x - output_region.x, position.y - output_region.y);
state.dragging = Some((output_key, output_region)); state.dragging = Some((output_key, output_region));
return event::Status::Captured; shell.capture_event();
} }
} }
} }
@ -239,7 +239,8 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
} }
} }
return event::Status::Captured; shell.capture_event();
return;
} }
if let Some(ref on_placement) = self.on_placement { if let Some(ref on_placement) = self.on_placement {
@ -253,14 +254,12 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
)); ));
} }
return event::Status::Captured; shell.capture_event();
} }
} }
_ => (), _ => (),
} }
event::Status::Ignored
} }
fn mouse_interaction( fn mouse_interaction(
@ -333,6 +332,7 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
width: 3.0, width: 3.0,
}, },
shadow: Default::default(), shadow: Default::default(),
snap: true,
}, },
core::Background::Color(background.into()), core::Background::Color(background.into()),
); );
@ -352,6 +352,7 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
..Default::default() ..Default::default()
}, },
shadow: Default::default(), shadow: Default::default(),
snap: true,
}, },
core::Background::Color(cosmic_theme.palette.neutral_1.into()), core::Background::Color(cosmic_theme.palette.neutral_1.into()),
); );
@ -364,8 +365,8 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
line_height: core::text::LineHeight::Relative(1.2), line_height: core::text::LineHeight::Relative(1.2),
font: cosmic::font::bold(), font: cosmic::font::bold(),
bounds: id_bounds.size(), bounds: id_bounds.size(),
horizontal_alignment: alignment::Horizontal::Center, align_x: text::Alignment::Center,
vertical_alignment: alignment::Vertical::Center, align_y: alignment::Vertical::Center,
shaping: text::Shaping::Basic, shaping: text::Shaping::Basic,
wrapping: text::Wrapping::Word, wrapping: text::Wrapping::Word,
ellipsize: text::Ellipsize::None, ellipsize: text::Ellipsize::None,

View file

@ -287,7 +287,7 @@ impl page::Page<crate::pages::Message> for Page {
// Forward messages from another thread to prevent the monitoring thread from blocking. // Forward messages from another thread to prevent the monitoring thread from blocking.
let (randr_task, randr_handle) = Task::stream(cosmic::iced_futures::stream::channel( let (randr_task, randr_handle) = Task::stream(cosmic::iced_futures::stream::channel(
1, 1,
|mut emitter| async move { |mut emitter: futures::channel::mpsc::Sender<_>| async move {
while let Some(message) = rx.recv().await { while let Some(message) = rx.recv().await {
if let cosmic_randr::Message::ManagerDone = message if let cosmic_randr::Message::ManagerDone = message
&& !refreshing_page.swap(true, Ordering::SeqCst) && !refreshing_page.swap(true, Ordering::SeqCst)
@ -359,7 +359,7 @@ impl page::Page<crate::pages::Message> for Page {
// Forward messages from the DRM hotplug thread. // Forward messages from the DRM hotplug thread.
let (hotplug_task, hotplug_handle) = Task::stream(cosmic::iced_futures::stream::channel( let (hotplug_task, hotplug_handle) = Task::stream(cosmic::iced_futures::stream::channel(
1, 1,
|mut emitter| async move { |mut emitter: futures::channel::mpsc::Sender<pages::Message>| async move {
while let Some(message) = rx.recv().await { while let Some(message) = rx.recv().await {
_ = emitter.send(message).await; _ = emitter.send(message).await;
} }
@ -609,8 +609,8 @@ impl Page {
return cosmic::iced::widget::scrollable::snap_to( return cosmic::iced::widget::scrollable::snap_to(
self.display_arrangement_scrollable.clone(), self.display_arrangement_scrollable.clone(),
RelativeOffset { RelativeOffset {
x: self.last_pan, x: Some(self.last_pan),
y: 0.0, y: None,
}, },
); );
} }
@ -662,7 +662,10 @@ impl Page {
self.last_pan = 0.5; self.last_pan = 0.5;
cosmic::iced::widget::scrollable::snap_to( cosmic::iced::widget::scrollable::snap_to(
self.display_arrangement_scrollable.clone(), self.display_arrangement_scrollable.clone(),
RelativeOffset { x: 0.5, y: 0.5 }, RelativeOffset {
x: Some(0.5),
y: Some(0.5),
},
) )
} }

View file

@ -801,46 +801,52 @@ fn keyboard_typing_assist() -> Section<crate::pages::Message> {
settings::section() settings::section()
.title(&section.title) .title(&section.title)
.add(settings::flex_item(&descriptions[repeat_delay], { .add(
// Delay settings::flex_item(&descriptions[repeat_delay], {
let delay_slider = cosmic::widget::slider( // Delay
KB_REPEAT_DELAY_MIN..=KB_REPEAT_DELAY_MAX, let delay_slider = cosmic::widget::slider(
page.xkb.repeat_delay, KB_REPEAT_DELAY_MIN..=KB_REPEAT_DELAY_MAX,
Message::SetRepeatKeysDelay, page.xkb.repeat_delay,
) Message::SetRepeatKeysDelay,
.width(Length::Fill) )
.breakpoints(&[KB_REPEAT_DELAY_DEFAULT]) .width(Length::Fill)
.step(50_u32) .breakpoints(&[KB_REPEAT_DELAY_DEFAULT])
.apply(widget::container) .step(50_u32)
.max_width(250); .apply(widget::container)
.max_width(250);
row::with_capacity(3) row::with_capacity(3)
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(theme::spacing().space_s) .spacing(theme::spacing().space_s)
.push(widget::text::body(&descriptions[short])) .push(widget::text::body(&descriptions[short]))
.push(delay_slider) .push(delay_slider)
.push(widget::text::body(&descriptions[long])) .push(widget::text::body(&descriptions[long]))
})) })
.add(settings::flex_item(&descriptions[repeat_rate], { .align_items(Alignment::Center),
// Repeat rate )
let rate_slider = cosmic::widget::slider( .add(
KB_REPEAT_RATE_MIN..=KB_REPEAT_RATE_MAX, settings::flex_item(&descriptions[repeat_rate], {
page.xkb.repeat_rate, // Repeat rate
Message::SetRepeatKeysRate, let rate_slider = cosmic::widget::slider(
) KB_REPEAT_RATE_MIN..=KB_REPEAT_RATE_MAX,
.width(Length::Fill) page.xkb.repeat_rate,
.breakpoints(&[KB_REPEAT_RATE_DEFAULT]) Message::SetRepeatKeysRate,
.step(5_u32) )
.apply(widget::container) .width(Length::Fill)
.max_width(250); .breakpoints(&[KB_REPEAT_RATE_DEFAULT])
.step(5_u32)
.apply(widget::container)
.max_width(250);
row::with_capacity(3) row::with_capacity(3)
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(theme::spacing().space_s) .spacing(theme::spacing().space_s)
.push(widget::text::body(&descriptions[slow])) .push(widget::text::body(&descriptions[slow]))
.push(rate_slider) .push(rate_slider)
.push(widget::text::body(&descriptions[fast])) .push(widget::text::body(&descriptions[fast]))
})) })
.align_items(Alignment::Center),
)
.apply(cosmic::Element::from) .apply(cosmic::Element::from)
.map(crate::pages::Message::Keyboard) .map(crate::pages::Message::Keyboard)
}) })

View file

@ -882,6 +882,7 @@ fn shortcut_item(custom: bool, id: usize, data: &ShortcutModel) -> Element<'_, S
settings::item::builder(&data.description) settings::item::builder(&data.description)
.flex_control(control) .flex_control(control)
.align_items(Alignment::Center)
.spacing(16) .spacing(16)
.apply(widget::container) .apply(widget::container)
.class(theme::Container::List) .class(theme::Container::List)

View file

@ -437,7 +437,7 @@ fn shortcuts() -> Section<crate::pages::Message> {
let descriptions = &section.descriptions; let descriptions = &section.descriptions;
let search = widget::search_input("", &page.search.input) let search = widget::search_input("", &page.search.input)
.width(314) .width(314.)
.on_clear(Message::Search(String::new())) .on_clear(Message::Search(String::new()))
.on_input(Message::Search) .on_input(Message::Search)
.apply(widget::container) .apply(widget::container)
@ -516,6 +516,7 @@ fn category_item(category: Category, name: &str, modified: u16) -> Element<'_, M
.class(theme::Container::List) .class(theme::Container::List)
.apply(widget::button::custom) .apply(widget::button::custom)
.class(theme::Button::Transparent) .class(theme::Button::Transparent)
.width(Length::Fill)
.on_press(Message::Category(category)) .on_press(Message::Category(category))
.into() .into()
} }

View file

@ -62,12 +62,15 @@ fn mouse() -> Section<crate::pages::Message> {
settings::section() settings::section()
.title(&section.title) .title(&section.title)
.add(settings::flex_item( .add(
&descriptions[primary_button], settings::flex_item(
cosmic::widget::segmented_control::horizontal(&input.primary_button) &descriptions[primary_button],
.minimum_button_width(0) cosmic::widget::segmented_control::horizontal(&input.primary_button)
.on_activate(|x| Message::PrimaryButtonSelected(x, false)), .minimum_button_width(0)
)) .on_activate(|x| Message::PrimaryButtonSelected(x, false)),
)
.align_items(Alignment::Center),
)
.add( .add(
settings::item::builder(&descriptions[mouse_speed]).flex_control({ settings::item::builder(&descriptions[mouse_speed]).flex_control({
let value = (input let value = (input
@ -130,35 +133,38 @@ fn scrolling() -> Section<crate::pages::Message> {
settings::section() settings::section()
.title(&section.title) .title(&section.title)
.add(settings::flex_item(&descriptions[scroll_speed], { .add(
let value = input settings::flex_item(&descriptions[scroll_speed], {
.input_default let value = input
.scroll_config .input_default
.as_ref() .scroll_config
.and_then(|x| x.scroll_factor) .as_ref()
.unwrap_or(1.) .and_then(|x| x.scroll_factor)
.log(2.) .unwrap_or(1.)
* 10.0 .log(2.)
+ 50.0; * 10.0
+ 50.0;
let slider = widget::slider(1.0..=100.0, value, |value| { let slider = widget::slider(1.0..=100.0, value, |value| {
Message::SetScrollFactor(2f64.powf((value - 50.0) / 10.0), false) Message::SetScrollFactor(2f64.powf((value - 50.0) / 10.0), false)
})
.width(Length::Fill)
.breakpoints(&[50.0])
.apply(widget::container)
.max_width(250);
row::with_capacity(2)
.align_y(Alignment::Center)
.spacing(8)
.push(
text::body(format!("{:.0}", value.round()))
.width(Length::Fixed(22.0))
.align_x(Alignment::Center),
)
.push(slider)
}) })
.width(Length::Fill) .align_items(Alignment::Center),
.breakpoints(&[50.0]) )
.apply(widget::container)
.max_width(250);
row::with_capacity(2)
.align_y(Alignment::Center)
.spacing(8)
.push(
text::body(format!("{:.0}", value.round()))
.width(Length::Fixed(22.0))
.align_x(Alignment::Center),
)
.push(slider)
}))
.add( .add(
settings::item::builder(&descriptions[natural]) settings::item::builder(&descriptions[natural])
.description(&descriptions[natural_desc]) .description(&descriptions[natural_desc])

View file

@ -9,12 +9,11 @@ use std::sync::{Arc, LazyLock};
use anyhow::Context; use anyhow::Context;
use cosmic::dialog::file_chooser::FileFilter; use cosmic::dialog::file_chooser::FileFilter;
use cosmic::task; use cosmic::task;
use cosmic::widget::text_input::focus;
use cosmic::{ use cosmic::{
Apply, Element, Task, Apply, Element, Task,
iced::{Alignment, Length}, iced::{Alignment, Length},
iced_core::text::Wrapping, iced_core::text::Wrapping,
widget::{self, icon}, widget::{self, icon, space::horizontal as horizontal_space, text_input::focus},
}; };
use cosmic_settings_network_manager_subscription::nm_secret_agent::{self, PasswordFlag}; use cosmic_settings_network_manager_subscription::nm_secret_agent::{self, PasswordFlag};
use cosmic_settings_network_manager_subscription::{ use cosmic_settings_network_manager_subscription::{
@ -1086,7 +1085,7 @@ fn devices_view() -> Section<crate::pages::Message> {
let widget = widget::settings::item_row(vec![ let widget = widget::settings::item_row(vec![
identifier.into(), identifier.into(),
widget::horizontal_space().into(), horizontal_space().into(),
controls.into(), controls.into(),
]); ]);

View file

@ -10,11 +10,10 @@ use anyhow::Context;
use cosmic::{ use cosmic::{
Apply, Element, Task, Apply, Element, Task,
app::ContextDrawer, app::ContextDrawer,
iced::{Alignment, Length}, iced::{Alignment, Length, widget::operation::focus_next},
iced_core::text::Wrapping, iced_core::text::Wrapping,
iced_widget::focus_next,
task, task,
widget::{self, column, icon, text_input::focus}, widget::{self, column, icon, space::horizontal as horizontal_space, text_input::focus},
}; };
use cosmic_settings_network_manager_subscription::{ use cosmic_settings_network_manager_subscription::{
self as network_manager, NetworkManagerState, self as network_manager, NetworkManagerState,
@ -1020,7 +1019,7 @@ fn devices_view() -> Section<crate::pages::Message> {
let item = widget::settings::item_row(vec![ let item = widget::settings::item_row(vec![
identifier.into(), identifier.into(),
widget::horizontal_space().into(), horizontal_space().into(),
controls.into(), controls.into(),
]); ]);
@ -1125,7 +1124,7 @@ fn devices_view() -> Section<crate::pages::Message> {
let item = widget::settings::item_row(vec![ let item = widget::settings::item_row(vec![
identifier.into(), identifier.into(),
widget::horizontal_space().into(), horizontal_space().into(),
controls.into(), controls.into(),
]); ]);
@ -1235,7 +1234,7 @@ fn devices_view() -> Section<crate::pages::Message> {
let item = widget::settings::item_row(vec![ let item = widget::settings::item_row(vec![
identifier.into(), identifier.into(),
widget::horizontal_space().into(), horizontal_space().into(),
connect, connect,
]); ]);

View file

@ -8,7 +8,7 @@ use cosmic::{
Apply, Element, Task, Apply, Element, Task,
iced::{Alignment, Length}, iced::{Alignment, Length},
iced_core::text::Wrapping, iced_core::text::Wrapping,
widget::{self, icon}, widget::{self, icon, space::horizontal as horizontal_space},
}; };
use cosmic_dbus_networkmanager::interface::enums::DeviceState; use cosmic_dbus_networkmanager::interface::enums::DeviceState;
use cosmic_settings_network_manager_subscription::{ use cosmic_settings_network_manager_subscription::{
@ -549,7 +549,7 @@ impl Page {
let widget = widget::settings::item_row(vec![ let widget = widget::settings::item_row(vec![
identifier.into(), identifier.into(),
widget::horizontal_space().into(), horizontal_space().into(),
controls.into(), controls.into(),
]); ]);

View file

@ -5,7 +5,7 @@ use backend::{Battery, ConnectedDevice, PowerProfile};
use cosmic::iced::{self, Alignment, Length}; use cosmic::iced::{self, Alignment, Length};
use cosmic::iced_widget::{column, row}; use cosmic::iced_widget::{column, row};
use cosmic::widget::{self, radio, settings, text}; use cosmic::widget::{self, radio, settings, space::horizontal as horizontal_space, text};
use cosmic::{Apply, surface}; use cosmic::{Apply, surface};
use cosmic::{Task, iced_futures}; use cosmic::{Task, iced_futures};
use cosmic_config::{Config, CosmicConfigEntry}; use cosmic_config::{Config, CosmicConfigEntry};
@ -15,6 +15,7 @@ use futures::{SinkExt, StreamExt};
use itertools::Itertools; use itertools::Itertools;
use slab::Slab; use slab::Slab;
use slotmap::SlotMap; use slotmap::SlotMap;
use std::hash::Hash;
use std::iter; use std::iter;
use std::time::Duration; use std::time::Duration;
use upower_dbus::DeviceProxy; use upower_dbus::DeviceProxy;
@ -153,24 +154,50 @@ impl page::Page<crate::pages::Message> for Page {
}); });
// Subscriptions for all connected device batteries. // Subscriptions for all connected device batteries.
let device_batteries = self let device_batteries =
.connected_devices self.connected_devices
.iter() .iter()
.filter_map(|device| { .filter_map(|device| {
device device
.proxy .proxy
.clone() .clone()
.map(|p| (device.device_path.clone(), p)) .map(|p| (device.device_path.clone(), p))
}) })
.map(|(path, proxy)| { .map(|(path, proxy)| {
iced::Subscription::run_with_id( #[derive(Clone)]
path.clone(), struct DeviceBatterySubscriptionData {
iced_futures::stream::channel(1, |sender| async move { proxy: DeviceProxy<'static>,
receive_battery_changes(proxy, path, sender, Message::UpdateDeviceBattery) path: String,
.await }
}),
) impl Hash for DeviceBatterySubscriptionData {
}); fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}
iced::Subscription::run_with(
DeviceBatterySubscriptionData { proxy, path },
|DeviceBatterySubscriptionData { proxy, path }| {
let path = path.clone();
let proxy = proxy.clone();
iced_futures::stream::channel(
1,
move |sender: futures::channel::mpsc::Sender<
crate::pages::Message,
>| async move {
receive_battery_changes(
proxy,
path,
sender,
Message::UpdateDeviceBattery,
)
.await
},
)
},
)
});
iced::Subscription::batch(std::iter::once(system_battery).chain(device_batteries)) iced::Subscription::batch(std::iter::once(system_battery).chain(device_batteries))
} }
@ -199,47 +226,56 @@ impl page::Page<crate::pages::Message> for Page {
} }
}), }),
cosmic::Task::run( cosmic::Task::run(
iced_futures::stream::channel(1, |mut emitter| async move { iced_futures::stream::channel(
let span = tracing::span!(tracing::Level::INFO, "power::device_stream task"); 1,
let _span_handle = span.enter(); |mut emitter: futures::channel::mpsc::Sender<Message>| async move {
let span =
tracing::span!(tracing::Level::INFO, "power::device_stream task");
let _span_handle = span.enter();
let Ok(connection) = zbus::Connection::system().await else { let Ok(connection) = zbus::Connection::system().await else {
tracing::error!("could not established zbus connection to system"); tracing::error!("could not established zbus connection to system");
return; return;
}; };
let added_stream = ConnectedDevice::device_added_stream(&connection).await; let added_stream = ConnectedDevice::device_added_stream(&connection).await;
let removed_stream = ConnectedDevice::device_removed_stream(&connection).await; let removed_stream =
ConnectedDevice::device_removed_stream(&connection).await;
let mut sender = emitter.clone(); let mut sender = emitter.clone();
let added_future = std::pin::pin!(async { let added_future = std::pin::pin!(async {
match added_stream { match added_stream {
Ok(stream) => { Ok(stream) => {
let mut stream = std::pin::pin!(stream); let mut stream = std::pin::pin!(stream);
while let Some(device) = stream.next().await { while let Some(device) = stream.next().await {
tracing::debug!(device = device.model, "device added"); tracing::debug!(device = device.model, "device added");
_ = sender.send(Message::DeviceConnect(device)).await; _ = sender.send(Message::DeviceConnect(device)).await;
}
}
Err(err) => tracing::error!(?err, "cannot establish added stream"),
}
});
let removed_future = std::pin::pin!(async {
match removed_stream {
Ok(stream) => {
let mut stream = std::pin::pin!(stream);
while let Some(device_path) = stream.next().await {
tracing::debug!(device_path, "device removed");
_ = emitter
.send(Message::DeviceDisconnect(device_path))
.await;
}
}
Err(err) => {
tracing::error!(?err, "cannot establish removed stream")
} }
} }
Err(err) => tracing::error!(?err, "cannot establish added stream"), });
}
});
let removed_future = std::pin::pin!(async { futures::future::select(added_future, removed_future).await;
match removed_stream { },
Ok(stream) => { ),
let mut stream = std::pin::pin!(stream);
while let Some(device_path) = stream.next().await {
tracing::debug!(device_path, "device removed");
_ = emitter.send(Message::DeviceDisconnect(device_path)).await;
}
}
Err(err) => tracing::error!(?err, "cannot establish removed stream"),
}
});
futures::future::select(added_future, removed_future).await;
}),
|msg| msg, |msg| msg,
), ),
]; ];
@ -449,7 +485,7 @@ fn connected_devices() -> Section<crate::pages::Message> {
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill), .height(Length::Fill),
) )
.height(64) .height(64.)
.class(cosmic::theme::Container::List) .class(cosmic::theme::Container::List)
.into() .into()
}) })
@ -469,14 +505,10 @@ fn connected_devices() -> Section<crate::pages::Message> {
cosmic::Element::from( cosmic::Element::from(
row!( row!(
device_row.next().unwrap_or( device_row.next().unwrap_or(
widget::horizontal_space() horizontal_space().width(Length::Fill).into()
.width(Length::Fill)
.into()
), ),
device_row.next().unwrap_or( device_row.next().unwrap_or(
widget::horizontal_space() horizontal_space().width(Length::Fill).into()
.width(Length::Fill)
.into()
), ),
) )
.spacing(8), .spacing(8),

View file

@ -7,7 +7,7 @@ use cosmic::{
Apply, Element, Task, Apply, Element, Task,
iced::{Alignment, Length, window}, iced::{Alignment, Length, window},
surface, surface,
widget::{self, settings}, widget::{self, settings, space::horizontal as horizontal_space},
}; };
use cosmic_config::{Config, ConfigGet, ConfigSet}; use cosmic_config::{Config, ConfigGet, ConfigSet};
use cosmic_settings_page::{self as page, Section, section}; use cosmic_settings_page::{self as page, Section, section};
@ -275,7 +275,10 @@ fn input() -> Section<crate::pages::Message> {
widget::slider(0..=100, page.model.source_volume, |change| { widget::slider(0..=100, page.model.source_volume, |change| {
Message::SetSourceVolume(change).into() Message::SetSourceVolume(change).into()
}) })
}; }
.width(Length::Fill)
.apply(widget::container)
.max_width(250.);
let volume_control = widget::row::with_capacity(4) let volume_control = widget::row::with_capacity(4)
.align_y(Alignment::Center) .align_y(Alignment::Center)
@ -292,7 +295,7 @@ fn input() -> Section<crate::pages::Message> {
.width(Length::Fixed(22.0)) .width(Length::Fixed(22.0))
.align_x(Alignment::Center), .align_x(Alignment::Center),
) )
.push(widget::horizontal_space().width(8)) .push(horizontal_space().width(8.))
.push(slider); .push(slider);
let devices = widget::dropdown::popup_dropdown( let devices = widget::dropdown::popup_dropdown(
page.model.sources(), page.model.sources(),
@ -307,10 +310,11 @@ fn input() -> Section<crate::pages::Message> {
let mut controls = settings::section() let mut controls = settings::section()
.title(&section.title) .title(&section.title)
.add(settings::flex_item( .add(
&*section.descriptions[volume], settings::item::builder(&*section.descriptions[volume])
volume_control, .flex_control(volume_control)
)) .align_items(Alignment::Center),
)
.add(settings::item(&*section.descriptions[device], devices)); .add(settings::item(&*section.descriptions[device], devices));
controls = controls.add( controls = controls.add(
@ -351,7 +355,10 @@ fn output() -> Section<crate::pages::Message> {
widget::slider(0..=100, page.model.sink_volume, |change| { widget::slider(0..=100, page.model.sink_volume, |change| {
Message::SetSinkVolume(change).into() Message::SetSinkVolume(change).into()
}) })
}; }
.width(Length::Fill)
.apply(widget::container)
.max_width(250.);
let volume_control = widget::row::with_capacity(4) let volume_control = widget::row::with_capacity(4)
.align_y(Alignment::Center) .align_y(Alignment::Center)
@ -368,7 +375,7 @@ fn output() -> Section<crate::pages::Message> {
.width(Length::Fixed(22.0)) .width(Length::Fixed(22.0))
.align_x(Alignment::Center), .align_x(Alignment::Center),
) )
.push(widget::horizontal_space().width(8)) .push(horizontal_space().width(8.))
.push(slider); .push(slider);
let devices = widget::dropdown::popup_dropdown( let devices = widget::dropdown::popup_dropdown(
@ -384,10 +391,11 @@ fn output() -> Section<crate::pages::Message> {
let mut controls = settings::section() let mut controls = settings::section()
.title(&section.title) .title(&section.title)
.add(settings::flex_item( .add(
&*section.descriptions[volume], settings::item::builder(&*section.descriptions[volume])
volume_control, .flex_control(volume_control)
)) .align_items(Alignment::Center),
)
.add(settings::item(&*section.descriptions[device], devices)) .add(settings::item(&*section.descriptions[device], devices))
.add(settings::item( .add(settings::item(
&*section.descriptions[balance], &*section.descriptions[balance],
@ -398,7 +406,7 @@ fn output() -> Section<crate::pages::Message> {
.width(Length::Fixed(22.0)) .width(Length::Fixed(22.0))
.align_x(Alignment::Center), .align_x(Alignment::Center),
) )
.push(widget::horizontal_space().width(8)) .push(horizontal_space().width(8.))
.push( .push(
widget::slider( widget::slider(
0..=200, 0..=200,
@ -408,7 +416,7 @@ fn output() -> Section<crate::pages::Message> {
) )
.breakpoints(&[100]), .breakpoints(&[100]),
) )
.push(widget::horizontal_space().width(8)) .push(horizontal_space().width(8.))
.push( .push(
widget::text::body(&*section.descriptions[right]) widget::text::body(&*section.descriptions[right])
.width(Length::Fixed(22.0)) .width(Length::Fixed(22.0))
@ -440,7 +448,7 @@ fn device_profiles() -> Section<crate::pages::Message> {
.view::<Page>(move |_binder, page, section| { .view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions; let descriptions = &section.descriptions;
let button = widget::row::with_children(vec![ let button = widget::row::with_children(vec![
widget::horizontal_space().into(), horizontal_space().into(),
widget::icon::from_name("go-next-symbolic").size(16).into(), widget::icon::from_name("go-next-symbolic").size(16).into(),
]); ]);
@ -448,10 +456,13 @@ fn device_profiles() -> Section<crate::pages::Message> {
.control(button) .control(button)
.spacing(16) .spacing(16)
.apply(widget::container) .apply(widget::container)
.width(Length::Fill)
.class(cosmic::theme::Container::List) .class(cosmic::theme::Container::List)
.apply(widget::button::custom) .apply(widget::button::custom)
.width(Length::Fill)
.class(cosmic::theme::Button::Transparent) .class(cosmic::theme::Button::Transparent)
.on_press(crate::pages::Message::Page(page.device_profiles)); .on_press(crate::pages::Message::Page(page.device_profiles))
.width(Length::Fill);
settings::section().add(device_profiles).into() settings::section().add(device_profiles).into()
}) })

View file

@ -1,6 +1,7 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cosmic::iced::Alignment;
use cosmic_settings_page::{self as page, Section, section}; use cosmic_settings_page::{self as page, Section, section};
use super::info::Info; use super::info::Info;
@ -177,7 +178,7 @@ fn device() -> Section<crate::pages::Message> {
page.editing_device_name, page.editing_device_name,
Message::HostnameEdit, Message::HostnameEdit,
) )
.width(250) .width(250.)
.on_input(Message::HostnameInput) .on_input(Message::HostnameInput)
.on_unfocus(Message::HostnameSubmit) .on_unfocus(Message::HostnameSubmit)
.on_submit(|_| Message::HostnameSubmit); .on_submit(|_| Message::HostnameSubmit);
@ -210,31 +211,34 @@ fn hardware() -> Section<crate::pages::Message> {
let mut section_builder = settings::section() let mut section_builder = settings::section()
.title(&section.title) .title(&section.title)
.add(settings::flex_item( .add(
&*desc[model], settings::flex_item(&*desc[model], text::body(&page.info.hardware_model))
text::body(&page.info.hardware_model), .align_items(Alignment::Center),
)) )
.add(settings::flex_item( .add(
&*desc[memory], settings::flex_item(&*desc[memory], text::body(&page.info.memory))
text::body(&page.info.memory), .align_items(Alignment::Center),
)) )
.add(settings::flex_item( .add(
&*desc[processor], settings::flex_item(&*desc[processor], text::body(&page.info.processor))
text::body(&page.info.processor), .align_items(Alignment::Center),
)); );
for card in &page.info.graphics { for card in &page.info.graphics {
section_builder = section_builder.add(settings::flex_item( section_builder = section_builder.add(
&*desc[graphics], settings::flex_item(&*desc[graphics], text::body(card.as_str()))
text::body(card.as_str()), .align_items(Alignment::Center),
)); );
} }
section_builder section_builder
.add(settings::flex_item( .add(
&*desc[disk_capacity], settings::flex_item(
text::body(&page.info.disk_capacity), &*desc[disk_capacity],
)) text::body(&page.info.disk_capacity),
)
.align_items(Alignment::Center),
)
.into() .into()
}) })
} }
@ -255,26 +259,32 @@ fn os() -> Section<crate::pages::Message> {
let desc = &section.descriptions; let desc = &section.descriptions;
settings::section() settings::section()
.title(&section.title) .title(&section.title)
.add(settings::flex_item( .add(
&*desc[os], settings::flex_item(&*desc[os], text::body(&page.info.operating_system))
text::body(&page.info.operating_system), .align_items(Alignment::Center),
)) )
.add(settings::flex_item( .add(
&*desc[os_arch], settings::flex_item(&*desc[os_arch], text::body(&page.info.os_architecture))
text::body(&page.info.os_architecture), .align_items(Alignment::Center),
)) )
.add(settings::flex_item( .add(
&*desc[kernel], settings::flex_item(&*desc[kernel], text::body(&page.info.kernel_version))
text::body(&page.info.kernel_version), .align_items(Alignment::Center),
)) )
.add(settings::flex_item( .add(
&*desc[desktop], settings::flex_item(
text::body(&page.info.desktop_environment), &*desc[desktop],
)) text::body(&page.info.desktop_environment),
.add(settings::flex_item( )
&*desc[windowing_system], .align_items(Alignment::Center),
text::body(&page.info.windowing_system), )
)) .add(
settings::flex_item(
&*desc[windowing_system],
text::body(&page.info.windowing_system),
)
.align_items(Alignment::Center),
)
.into() .into()
}) })
} }

View file

@ -8,7 +8,7 @@ use cosmic::{
Apply, Element, Apply, Element,
dialog::file_chooser, dialog::file_chooser,
iced::{Alignment, Length}, iced::{Alignment, Length},
widget::{self, Space, column, icon, row, settings, text}, widget::{self, column, icon, row, settings, space::horizontal as horizontal_space, text},
}; };
use cosmic_settings_page::{self as page, Section, section}; use cosmic_settings_page::{self as page, Section, section};
use image::GenericImageView; use image::GenericImageView;
@ -322,7 +322,7 @@ impl page::Page<crate::pages::Message> for Page {
))) )))
.width(Length::Fill), .width(Length::Fill),
) )
.push(Space::new(5, 0)) .push(horizontal_space().width(5.))
.push(admin_toggler) .push(admin_toggler)
.align_y(Alignment::Center), .align_y(Alignment::Center),
), ),
@ -843,7 +843,7 @@ fn user_list() -> Section<crate::pages::Message> {
.push(text::caption(crate::fl!("administrator", "desc"))) .push(text::caption(crate::fl!("administrator", "desc")))
.width(Length::Fill) .width(Length::Fill)
.into(), .into(),
Space::new(5, 0).into(), horizontal_space().width(5.).into(),
widget::toggler(user.is_admin) widget::toggler(user.is_admin)
.on_toggle(|enabled| { .on_toggle(|enabled| {
Message::SelectedUserSetAdmin(user.id, enabled) Message::SelectedUserSetAdmin(user.id, enabled)
@ -853,7 +853,7 @@ fn user_list() -> Section<crate::pages::Message> {
if page.users.len() > 1 { if page.users.len() > 1 {
details_list = details_list.add(settings::item_row(vec![ details_list = details_list.add(settings::item_row(vec![
widget::horizontal_space().width(Length::Fill).into(), horizontal_space().width(Length::Fill).into(),
widget::button::destructive(crate::fl!("remove-user")) widget::button::destructive(crate::fl!("remove-user"))
.on_press(Message::SelectedUserDelete(user.id)) .on_press(Message::SelectedUserDelete(user.id))
.into(), .into(),
@ -885,7 +885,7 @@ fn user_list() -> Section<crate::pages::Message> {
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(space_xxs) .spacing(space_xxs)
.into(), .into(),
widget::horizontal_space().width(Length::Fill).into(), horizontal_space().width(Length::Fill).into(),
icon::from_name(if expanded { icon::from_name(if expanded {
"go-up-symbolic" "go-up-symbolic"
} else { } else {
@ -901,6 +901,7 @@ fn user_list() -> Section<crate::pages::Message> {
.padding([space_xxs, space_m]) .padding([space_xxs, space_m])
.on_press(Message::SelectUser(idx)) .on_press(Message::SelectUser(idx))
.class(cosmic::theme::Button::ListItem) .class(cosmic::theme::Button::ListItem)
.width(Length::Fill)
.selected(expanded) .selected(expanded)
.apply(Element::from), .apply(Element::from),
); );

View file

@ -7,7 +7,7 @@ use cosmic::{
cosmic_config::{self, ConfigGet, ConfigSet}, cosmic_config::{self, ConfigGet, ConfigSet},
iced_core::text::Wrapping, iced_core::text::Wrapping,
surface, surface,
widget::{self, dropdown, settings}, widget::{self, dropdown, settings, space::horizontal as horizontal_space},
}; };
use cosmic_settings_page::{self as page, Section, section}; use cosmic_settings_page::{self as page, Section, section};
use icu::{ use icu::{
@ -366,7 +366,7 @@ impl Page {
.class(cosmic::theme::Svg::Custom(svg_accent.clone())) .class(cosmic::theme::Svg::Custom(svg_accent.clone()))
.into() .into()
} else { } else {
widget::horizontal_space().width(16).into() horizontal_space().width(16.).into()
}, },
]) ])
.apply(widget::container) .apply(widget::container)

View file

@ -8,7 +8,7 @@ use std::sync::Arc;
use cosmic::app::{ContextDrawer, context_drawer}; use cosmic::app::{ContextDrawer, context_drawer};
use cosmic::iced::{Alignment, Length}; use cosmic::iced::{Alignment, Length};
use cosmic::iced_core::text::Wrapping; use cosmic::iced_core::text::Wrapping;
use cosmic::widget::{self, button}; use cosmic::widget::{self, button, space::horizontal as horizontal_space};
use cosmic::{Apply, Element}; use cosmic::{Apply, Element};
use cosmic_config::{ConfigGet, ConfigSet}; use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_page::Section; use cosmic_settings_page::Section;
@ -398,7 +398,7 @@ impl Page {
.class(cosmic::theme::Svg::Custom(svg_accent.clone())) .class(cosmic::theme::Svg::Custom(svg_accent.clone()))
.into() .into()
} else { } else {
widget::horizontal_space().width(16).into() horizontal_space().width(16.).into()
}, },
]) ])
.apply(widget::container) .apply(widget::container)
@ -526,7 +526,7 @@ impl Page {
.class(cosmic::theme::Svg::Custom(svg_accent.clone())) .class(cosmic::theme::Svg::Custom(svg_accent.clone()))
.into() .into()
} else { } else {
widget::horizontal_space().width(16).into() horizontal_space().width(16.).into()
}, },
]) ])
.apply(widget::container) .apply(widget::container)

View file

@ -12,15 +12,14 @@ use tokio::select;
pub fn daytime() -> cosmic::iced::Subscription<bool> { pub fn daytime() -> cosmic::iced::Subscription<bool> {
struct Sunset; struct Sunset;
Subscription::run_with_id( Subscription::run_with(TypeId::of::<Sunset>(), |_| {
TypeId::of::<Sunset>(), stream::channel(2, |tx: Sender<bool>| async {
stream::channel(2, |tx| async {
if let Err(err) = inner(tx).await { if let Err(err) = inner(tx).await {
tracing::error!("Sunset subscription error: {:?}", err); tracing::error!("Sunset subscription error: {:?}", err);
} }
future::pending().await future::pending().await
}), })
) })
} }
enum Event { enum Event {

View file

@ -15,45 +15,49 @@ pub enum Event {
pub fn desktop_files<I: 'static + Hash + Copy + Send + Sync + Debug>( pub fn desktop_files<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> cosmic::iced::Subscription<Event> { ) -> cosmic::iced::Subscription<Event> {
Subscription::run_with_id( Subscription::run_with(id, |_| {
id, stream::channel(
stream::channel(1, move |mut output| async move { 1,
let handle = tokio::runtime::Handle::current(); move |mut output: futures::channel::mpsc::Sender<Event>| async move {
let (tx, mut rx) = mpsc::channel(4); let handle = tokio::runtime::Handle::current();
let mut last_update = std::time::Instant::now(); let (tx, mut rx) = mpsc::channel(4);
let mut last_update = std::time::Instant::now();
// Automatically select the best implementation for your platform. // Automatically select the best implementation for your platform.
// You can also access each implementation directly e.g. INotifyWatcher. // You can also access each implementation directly e.g. INotifyWatcher.
let watcher = RecommendedWatcher::new( let watcher = RecommendedWatcher::new(
move |res: Result<notify::Event, notify::Error>| { move |res: Result<notify::Event, notify::Error>| {
if let Ok(event) = res { if let Ok(event) = res {
match event.kind { match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => { EventKind::Create(_)
let now = std::time::Instant::now(); | EventKind::Modify(_)
if now.duration_since(last_update).as_secs() > 3 { | EventKind::Remove(_) => {
_ = handle.block_on(tx.send(())); let now = std::time::Instant::now();
last_update = now; if now.duration_since(last_update).as_secs() > 3 {
_ = handle.block_on(tx.send(()));
last_update = now;
}
} }
_ => (),
} }
_ => (),
} }
},
Config::default(),
);
if let Ok(mut watcher) = watcher {
for path in cosmic::desktop::fde::default_paths() {
let _ = watcher.watch(path.as_ref(), RecursiveMode::Recursive);
} }
},
Config::default(),
);
if let Ok(mut watcher) = watcher { while rx.recv().await.is_some() {
for path in cosmic::desktop::fde::default_paths() { _ = output.send(Event::Changed).await;
let _ = watcher.watch(path.as_ref(), RecursiveMode::Recursive); }
} }
while rx.recv().await.is_some() { futures::future::pending().await
_ = output.send(Event::Changed).await; },
} )
} })
futures::future::pending().await
}),
)
} }

View file

@ -25,15 +25,16 @@ pub enum WallpaperEvent {
} }
pub fn wallpapers(current_dir: PathBuf) -> cosmic::iced::Subscription<WallpaperEvent> { pub fn wallpapers(current_dir: PathBuf) -> cosmic::iced::Subscription<WallpaperEvent> {
Subscription::run_with_id( Subscription::run_with(current_dir, |current_dir: &PathBuf| {
current_dir.clone(), let current_dir = current_dir.clone();
stream::channel(2, |tx| async { stream::channel(2, move |tx: Sender<WallpaperEvent>| async move {
let current_dir = current_dir.clone();
if let Err(err) = inner(tx, current_dir).await { if let Err(err) = inner(tx, current_dir).await {
tracing::error!("Wallpapers subscription error: {:?}", err); tracing::error!("Wallpapers subscription error: {:?}", err);
} }
future::pending().await future::pending().await
}), })
) })
} }
async fn inner(tx: Sender<WallpaperEvent>, current_dir: PathBuf) -> anyhow::Result<()> { async fn inner(tx: Sender<WallpaperEvent>, current_dir: PathBuf) -> anyhow::Result<()> {

View file

@ -17,6 +17,7 @@ pub fn display_container_frame() -> cosmic::theme::Container<'static> {
width: 3.0, width: 3.0,
}, },
shadow: Default::default(), shadow: Default::default(),
snap: true,
} }
}) })
} }
@ -35,6 +36,7 @@ pub fn display_container_screen() -> cosmic::theme::Container<'static> {
width: 0.0, width: 0.0,
}, },
shadow: Default::default(), shadow: Default::default(),
snap: true,
} }
}) })
} }

View file

@ -8,8 +8,9 @@ use cosmic::iced::{Alignment, Length};
use cosmic::iced_core::text::Wrapping; use cosmic::iced_core::text::Wrapping;
use cosmic::widget::color_picker::ColorPickerUpdate; use cosmic::widget::color_picker::ColorPickerUpdate;
use cosmic::widget::{ use cosmic::widget::{
self, ColorPickerModel, button, column, container, divider, horizontal_space, icon, row, self, ColorPickerModel, button, column, container, divider, icon, row, settings,
settings, text, vertical_space, space::{horizontal as horizontal_space, vertical as vertical_space},
text,
}; };
use cosmic::{Apply, Element, theme}; use cosmic::{Apply, Element, theme};
use cosmic_settings_page as page; use cosmic_settings_page as page;
@ -150,10 +151,12 @@ pub fn page_list_item<'a, Message: 'static + Clone>(
.padding([space_s, space_m]) .padding([space_s, space_m])
.align_x(Alignment::Center) .align_x(Alignment::Center)
.class(theme::Container::List) .class(theme::Container::List)
.width(Length::Fill)
.apply(button::custom) .apply(button::custom)
.padding(0) .padding(0)
.class(theme::Button::Transparent) .class(theme::Button::Transparent)
.on_press(message) .on_press(message)
.width(Length::Fill)
.into() .into()
} }
@ -190,9 +193,12 @@ pub fn go_next_item<Msg: Clone + 'static>(
horizontal_space().into(), horizontal_space().into(),
icon::from_name("go-next-symbolic").size(16).icon().into(), icon::from_name("go-next-symbolic").size(16).icon().into(),
]) ])
.width(Length::Fill)
.apply(widget::container) .apply(widget::container)
.class(cosmic::theme::Container::List) .class(cosmic::theme::Container::List)
.width(Length::Fill)
.apply(button::custom) .apply(button::custom)
.width(Length::Fill)
.padding(0) .padding(0)
.class(theme::Button::Transparent) .class(theme::Button::Transparent)
.on_press_maybe(msg_opt.into()) .on_press_maybe(msg_opt.into())
@ -214,10 +220,13 @@ pub fn go_next_with_item<'a, Msg: Clone + 'static>(
.spacing(cosmic::theme::spacing().space_s) .spacing(cosmic::theme::spacing().space_s)
.into(), .into(),
]) ])
.width(Length::Fill)
.apply(widget::container) .apply(widget::container)
.class(cosmic::theme::Container::List) .class(cosmic::theme::Container::List)
.width(Length::Fill)
.apply(button::custom) .apply(button::custom)
.padding(0) .padding(0)
.width(Length::Fill)
.class(theme::Button::Transparent) .class(theme::Button::Transparent)
.on_press_maybe(msg_opt.into()) .on_press_maybe(msg_opt.into())
.into() .into()

View file

@ -34,16 +34,18 @@ pub struct State {
pub fn subscription() -> Subscription<Response> { pub fn subscription() -> Subscription<Response> {
struct MyId; struct MyId;
Subscription::run_with_id( Subscription::run_with(std::any::TypeId::of::<MyId>(), |_| {
std::any::TypeId::of::<MyId>(), stream::channel(
stream::channel(1, move |mut output| async move { 1,
if let Some(state) = State::new(&mut output).await { move |mut output: futures::channel::mpsc::Sender<Response>| async move {
state.listen(&mut output).await; if let Some(state) = State::new(&mut output).await {
} state.listen(&mut output).await;
}
futures::future::pending::<()>().await; futures::future::pending::<()>().await;
}), },
) )
})
} }
impl State { impl State {

View file

@ -6,8 +6,9 @@ use iced_futures::Subscription;
use std::collections::HashMap; use std::collections::HashMap;
pub fn subscription() -> iced_futures::Subscription<bool> { pub fn subscription() -> iced_futures::Subscription<bool> {
Subscription::run_with_id( struct MyId;
"airplane-mode",
Subscription::run_with(std::any::TypeId::of::<MyId>(), |_| {
async { async {
match rfkill::rfkill_updates() { match rfkill::rfkill_updates() {
Ok(updates) => updates.filter_map(|state| async { Ok(updates) => updates.filter_map(|state| async {
@ -25,8 +26,8 @@ pub fn subscription() -> iced_futures::Subscription<bool> {
} }
} }
} }
.flatten_stream(), .flatten_stream()
) })
} }
// Test that: // Test that:

View file

@ -1,6 +1,8 @@
// Copyright 2024 System76 <info@system76.com> // Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::Wrapper;
use super::Event; use super::Event;
use cosmic_dbus_networkmanager::nm::NetworkManager; use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
@ -18,13 +20,13 @@ pub fn active_conns_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>
id: I, id: I,
conn: Connection, conn: Connection,
) -> iced_futures::Subscription<Event> { ) -> iced_futures::Subscription<Event> {
Subscription::run_with_id( Subscription::run_with(Wrapper { id, conn: conn }, |Wrapper { id: _id, conn }| {
id, let conn = conn.clone();
stream::channel(50, move |output| async move { stream::channel(50, move |output| async move {
watch(conn, output).await; watch(conn, output).await;
futures::future::pending().await futures::future::pending().await
}), })
) })
} }
pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender<Event>) { pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender<Event>) {

View file

@ -6,6 +6,7 @@ pub use cosmic_dbus_networkmanager::interface::enums::{
ActiveConnectionState, DeviceState, DeviceType, ActiveConnectionState, DeviceState, DeviceType,
}; };
use core::hash;
use cosmic_dbus_networkmanager::nm::NetworkManager; use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use iced_futures::{self, Subscription, stream}; use iced_futures::{self, Subscription, stream};
@ -166,12 +167,35 @@ pub fn subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
has_popup: bool, has_popup: bool,
conn: Connection, conn: Connection,
) -> iced_futures::Subscription<Event> { ) -> iced_futures::Subscription<Event> {
Subscription::run_with_id( struct Wrapper<I> {
(id, has_popup), id: I,
stream::channel(50, move |output| async move { has_popup: bool,
watch(conn, has_popup, output).await; conn: Connection,
futures::future::pending().await }
}), impl<I: Hash> Hash for Wrapper<I> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.has_popup.hash(state);
}
}
Subscription::run_with(
Wrapper {
id,
has_popup,
conn,
},
|Wrapper {
id,
has_popup,
conn,
}| {
let conn = conn.clone();
let has_popup = *has_popup;
stream::channel(50, move |output| async move {
watch(conn, has_popup, output).await;
futures::future::pending().await
})
},
) )
} }

View file

@ -9,7 +9,7 @@ pub mod hw_address;
pub mod nm_secret_agent; pub mod nm_secret_agent;
pub mod wireless_enabled; pub mod wireless_enabled;
use std::{collections::HashMap, fmt::Debug, sync::Arc, time::Duration}; use std::{collections::HashMap, fmt::Debug, hash::Hash, sync::Arc, time::Duration};
use available_wifi::NetworkType; use available_wifi::NetworkType;
pub use cosmic_dbus_networkmanager as dbus; pub use cosmic_dbus_networkmanager as dbus;
@ -41,6 +41,17 @@ use self::{
pub type SSID = Arc<str>; pub type SSID = Arc<str>;
pub type UUID = Arc<str>; pub type UUID = Arc<str>;
pub(crate) struct Wrapper<I> {
id: I,
conn: zbus::Connection,
}
impl<I: Hash> Hash for Wrapper<I> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
#[error("failed to list bluetooth devices with rfkill")] #[error("failed to list bluetooth devices with rfkill")]
@ -121,13 +132,25 @@ pub fn subscription<I: Copy + Debug + std::hash::Hash + 'static>(
id: I, id: I,
conn: zbus::Connection, conn: zbus::Connection,
) -> iced_futures::Subscription<Event> { ) -> iced_futures::Subscription<Event> {
Subscription::run_with_id( struct Wrapper<I> {
id, id: I,
stream::channel(50, |output| async move { conn: zbus::Connection,
watch(conn, output).await; }
futures::future::pending().await impl<I: Hash> Hash for Wrapper<I> {
}), fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
) self.id.hash(state);
}
}
Subscription::run_with(Wrapper { id, conn }, |Wrapper { id, conn }| {
let conn = conn.clone();
stream::channel(
50,
|output: futures::channel::mpsc::Sender<Event>| async move {
watch(conn, output).await;
futures::future::pending().await
},
)
})
} }
pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender<Event>) { pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender<Event>) {

View file

@ -135,11 +135,15 @@ pub fn secret_agent_stream(
identifier: impl AsRef<str>, identifier: impl AsRef<str>,
rx: tokio::sync::mpsc::Receiver<Request>, rx: tokio::sync::mpsc::Receiver<Request>,
) -> impl Stream<Item = Event> { ) -> impl Stream<Item = Event> {
iced_futures::stream::channel(4, move |mut msg_tx| async move { iced_futures::stream::channel(
if let Err(e) = secret_agent_stream_impl(identifier.as_ref(), msg_tx.clone(), rx).await { 4,
let _ = msg_tx.send(Event::Failed(e)).await; move |mut msg_tx: futures::channel::mpsc::Sender<Event>| async move {
} if let Err(e) = secret_agent_stream_impl(identifier.as_ref(), msg_tx.clone(), rx).await
}) {
let _ = msg_tx.send(Event::Failed(e)).await;
}
},
)
} }
async fn secret_agent_stream_impl( async fn secret_agent_stream_impl(

View file

@ -1,6 +1,8 @@
// Copyright 2024 System76 <info@system76.com> // Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::Wrapper;
use super::Event; use super::Event;
use cosmic_dbus_networkmanager::nm::NetworkManager; use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
@ -18,13 +20,13 @@ pub fn wireless_enabled_subscription<I: 'static + Hash + Copy + Send + Sync + De
id: I, id: I,
conn: Connection, conn: Connection,
) -> iced_futures::Subscription<Event> { ) -> iced_futures::Subscription<Event> {
Subscription::run_with_id( Subscription::run_with(Wrapper { id, conn: conn }, |Wrapper { id: _id, conn }| {
id, let conn = conn.clone();
stream::channel(50, move |output| async move { stream::channel(50, move |output| async move {
watch(conn, output).await; watch(conn, output).await;
futures::future::pending().await futures::future::pending().await
}), })
) })
} }
pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender<Event>) { pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender<Event>) {

View file

@ -0,0 +1,12 @@
[package]
name = "cosmic-settings-pulse-subscription"
version = "0.1.0"
edition = "2024"
rust-version.workspace = true
[dependencies]
libpulse-binding = { version = "2.30.1" }
rustix = { version = "1.1.3", features = ["pipe"] }
iced_futures = { git = "https://github.com/pop-os/libcosmic" }
futures = "0.3.32"
log = "0.4.27"

View file

@ -0,0 +1,751 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
// Make sure not to fail if pulse not found, and reconnect?
// change to device shouldn't send osd?
use futures::{SinkExt, executor::block_on};
use iced_futures::{Subscription, stream};
use libpulse_binding::{
callbacks::ListResult,
channelmap::Map,
context::{
Context, FlagSet, State,
introspect::{CardInfo, CardProfileInfo, Introspector, ServerInfo, SinkInfo, SourceInfo},
subscribe::{Facility, InterestMaskSet, Operation},
},
def::{PortAvailable, Retval},
mainloop::{
api::MainloopApi,
events::io::IoEventInternal,
standard::{IterateResult, Mainloop},
},
volume::{ChannelVolumes, Volume},
};
use std::{
borrow::Cow,
cell::{Cell, RefCell},
convert::Infallible,
io::{Read, Write},
os::{
fd::{FromRawFd, IntoRawFd, RawFd},
raw::c_void,
},
rc::Rc,
str::FromStr,
sync::mpsc,
};
pub fn subscription() -> iced_futures::Subscription<Event> {
Subscription::run_with("pulse", |_| {
stream::channel(20, |sender| async {
std::thread::spawn(move || thread(sender));
futures::future::pending().await
})
})
}
pub fn thread(sender: futures::channel::mpsc::Sender<Event>) {
let Some(mut main_loop) = Mainloop::new() else {
log::error!("Failed to create PA main loop");
return;
};
let Some(mut context) = Context::new(&main_loop, "cosmic-osd") else {
log::error!("Failed to create PA context");
return;
};
let data = Rc::new(Data {
main_loop: RefCell::new(Mainloop {
_inner: Rc::clone(&main_loop._inner),
}),
introspector: context.introspect(),
sink_volume: Cell::new(None),
sink_mute: Cell::new(None),
source_volume: Cell::new(None),
source_mute: Cell::new(None),
default_sink_name: RefCell::new(None),
default_source_name: RefCell::new(None),
sender: RefCell::new(sender.clone()),
});
let data_clone = data.clone();
context.set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
data_clone.subscribe_cb(facility.unwrap(), operation, index);
})));
let _ = context.connect(None, FlagSet::NOFAIL, None);
loop {
if sender.is_closed() {
return;
}
match main_loop.iterate(false) {
IterateResult::Success(_) => {}
IterateResult::Err(_e) => {
return;
}
IterateResult::Quit(_e) => {
return;
}
}
if context.get_state() == State::Ready {
break;
}
}
// Inspect all available cards on startup
data.introspector.get_card_info_list({
let data_weak = Rc::downgrade(&data);
move |card_info_res| {
if let Some(data) = data_weak.upgrade() {
data.card_info_cb(card_info_res)
}
}
});
data.get_server_info();
context.subscribe(
InterestMaskSet::SERVER | InterestMaskSet::SINK | InterestMaskSet::SOURCE,
|_| {},
);
if let Err((err, retval)) = main_loop.run() {
log::error!("PA main loop returned {:?}, error {}", retval, err);
}
}
#[derive(Clone, Debug)]
pub enum Event {
Balance(Option<f32>),
CardInfo(Card),
DefaultSink(String),
DefaultSource(String),
SinkVolume(u32),
Channels(PulseChannels),
SinkMute(bool),
SourceVolume(u32),
SourceMute(bool),
}
enum Request {
Volume(u32, f32),
Balance(u32, f32),
Quit,
}
#[derive(Debug)]
pub struct PulseChannels {
tx: mpsc::Sender<Request>,
pipe_tx: std::fs::File,
index: u32,
}
impl Clone for PulseChannels {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
pipe_tx: self
.pipe_tx
.try_clone()
.expect("failed to clone PulseChannels pipe writer"),
index: self.index,
}
}
}
/// Data used by the [`handle_balance_io_new`] callback.
struct HandleBalanceData(
Context,
ChannelVolumes,
Map,
std::sync::mpsc::Receiver<Request>,
);
/// Callback for creating an IO event source [`MainloopApi::io_new`].
extern "C" fn handle_balance_io_new(
api: *const MainloopApi,
event: *mut IoEventInternal,
reader_fd: RawFd,
_flags: libpulse_binding::mainloop::events::io::FlagSet,
data: *mut c_void,
) {
// Take ownership of the data and borrow its contents.
let mut data = unsafe { Box::<HandleBalanceData>::from_raw(data as _) };
let HandleBalanceData(ctx, volumes, map, rx) = data.as_mut();
// Return early if the context is not ready, and give the data back.
if ctx.get_state() != State::Ready {
let _ = Box::leak(data);
return;
}
// If the first byte cannot be read, destroy this event source with its reader and data.
let mut buf = [0u8; 1];
let mut reader = unsafe { std::fs::File::from_raw_fd(reader_fd) };
if reader.read_exact(&mut buf).is_err() {
(unsafe { &*api })
.io_free
.as_ref()
.expect("io_free function is missing")(event);
return;
}
// Give ownership of the reader back.
_ = reader.into_raw_fd();
while let Ok(req) = rx.try_recv() {
match req {
Request::Volume(index, volume_scale) => {
let mut intro = ctx.introspect();
let new_scale = Volume((volume_scale * Volume::NORMAL.0 as f32).round() as u32);
if let Some(v) = volumes.scale(new_scale) {
_ = intro.set_sink_volume_by_index(
index,
v,
Some(Box::new(|success| {
if !success {
log::error!("Failed to set sink balance");
}
})),
);
}
}
Request::Balance(index, new_balance) => {
if map.can_balance() {
if let Some(v) = volumes.set_balance(&map, new_balance) {
let mut intro = ctx.introspect();
_ = intro.set_sink_volume_by_index(
index,
v,
Some(Box::new(|success| {
if !success {
log::error!("Failed to set sink balance");
}
})),
);
}
}
}
Request::Quit => unsafe { &*api }
.quit
.as_ref()
.expect("quit function missing")(api, 0),
}
}
let _ = Box::leak(data);
}
impl PulseChannels {
fn new(
volumes: ChannelVolumes,
map: Map,
api: &MainloopApi,
index: u32,
ctx: Context,
) -> PulseChannels {
let (reader, writer) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC)
.expect("failed to crate pipe");
let (tx, rx) = mpsc::channel::<Request>();
// Create IO event source object for handling speaker balance.
let event_source = api.io_new.as_ref().unwrap()(
api as *const _,
reader.into_raw_fd(),
libpulse_binding::mainloop::events::io::FlagSet::INPUT,
Some(handle_balance_io_new),
Box::into_raw(Box::new(HandleBalanceData(ctx, volumes, map, rx))) as *mut c_void,
);
if let Some(enable) = api.io_enable.as_ref() {
enable(
event_source,
libpulse_binding::mainloop::events::io::FlagSet::INPUT,
);
}
Self {
tx,
pipe_tx: std::fs::File::from(writer),
index,
}
}
/// Change the active index.
#[inline]
pub fn set_index(&mut self, index: u32) {
self.index = index;
}
/// Set the speaker balance of the active sink.
pub fn set_balance(&mut self, balance: f32) {
if let Err(err) = self.tx.send(Request::Balance(self.index, balance)) {
log::error!("Failed to send new balance to channel");
} else {
self.pipe_tx
.write_all(&[1])
.expect("PulseChannels pipe write failed");
}
}
/// Set the volume of the active sink.
pub fn set_volume(&mut self, volume: f32) {
if let Err(err) = self.tx.send(Request::Volume(self.index, volume)) {
log::error!("Failed to send new volume to channel");
} else {
self.pipe_tx
.write_all(&[1])
.expect("PulseChannels pipe write failed");
}
}
/// Request the pulse thread to quit.
pub fn quit(mut self) {
_ = self.tx.send(Request::Quit);
_ = self.pipe_tx.write_all(&[1]);
}
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct Card {
pub object_id: u32,
pub name: String,
pub product_name: String,
pub variant: DeviceVariant,
pub ports: Vec<CardPort>,
pub profiles: Vec<CardProfile>,
pub active_profile: Option<CardProfile>,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct CardPort {
pub name: String,
pub description: String,
pub direction: Direction,
pub port_type: PortType,
pub profile_port: u32,
pub priority: u32,
pub profiles: Vec<CardProfile>,
pub availability: Availability,
}
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub enum Availability {
Unknown,
No,
Yes,
}
impl From<PortAvailable> for Availability {
fn from(pa: PortAvailable) -> Self {
match pa {
PortAvailable::Unknown => Availability::Unknown,
PortAvailable::No => Availability::No,
PortAvailable::Yes => Availability::Yes,
}
}
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct CardProfile {
pub name: String,
pub description: String,
pub available: bool,
pub n_sinks: u32,
pub n_sources: u32,
pub priority: u32,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum DeviceVariant {
Alsa { alsa_card: u32 },
Bluez5 { address: String },
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum Direction {
Input,
Output,
Both,
}
#[derive(Default, Clone, Debug, Hash, Eq, PartialEq)]
pub enum PortType {
Mic,
Speaker,
Headphones,
Headset,
Digital,
#[default]
Unknown,
}
impl FromStr for PortType {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mic" => Ok(PortType::Mic),
"speaker" => Ok(PortType::Speaker),
"headphones" => Ok(PortType::Headphones),
"headset" => Ok(PortType::Headset),
"digital" => Ok(PortType::Digital),
_ => Ok(PortType::Unknown),
}
}
}
struct Data {
main_loop: RefCell<Mainloop>,
default_sink_name: RefCell<Option<String>>,
default_source_name: RefCell<Option<String>>,
sink_volume: Cell<Option<u32>>,
sink_mute: Cell<Option<bool>>,
source_volume: Cell<Option<u32>>,
source_mute: Cell<Option<bool>>,
introspector: Introspector,
sender: RefCell<futures::channel::mpsc::Sender<Event>>,
}
impl Data {
fn card_info_cb(self: &Rc<Self>, card_info: ListResult<&CardInfo>) {
if let ListResult::Item(card_info) = card_info {
let Some(object_id) = card_info
.proplist
.get_str("object.id")
.and_then(|v| v.parse::<u32>().ok())
else {
return;
};
let variant = if let Some(alsa_card) = card_info
.proplist
.get_str("alsa.card")
.and_then(|v| v.parse::<u32>().ok())
{
DeviceVariant::Alsa { alsa_card }
} else if let Some(address) = card_info.proplist.get_str("api.bluez5.address") {
DeviceVariant::Bluez5 { address }
} else {
return;
};
let card = Card {
name: card_info
.name
.as_ref()
.map(Cow::to_string)
.unwrap_or_default(),
product_name: card_info
.proplist
.get_str("device.product.name")
.unwrap_or_default(),
object_id,
variant,
ports: card_info
.ports
.iter()
.map(|port| CardPort {
name: port.name.as_ref().map(Cow::to_string).unwrap_or_default(),
description: port
.description
.as_ref()
.map(Cow::to_string)
.unwrap_or_default(),
direction: match port.direction.bits() {
x if x == libpulse_binding::direction::FlagSet::INPUT.bits() => {
Direction::Input
}
x if x == libpulse_binding::direction::FlagSet::OUTPUT.bits() => {
Direction::Output
}
_ => Direction::Both,
},
port_type: port
.proplist
.get_str("port.type")
.as_deref()
.map(|s| PortType::from_str(s).unwrap())
.unwrap_or_default(),
profile_port: port
.proplist
.get_str("card.profile.port")
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(0),
priority: port.priority,
profiles: collect_profiles(&port.profiles),
availability: port.available.into(),
})
.collect(),
profiles: collect_profiles(&card_info.profiles),
active_profile: card_info.active_profile.as_deref().map(CardProfile::from),
};
if block_on(self.sender.borrow_mut().send(Event::CardInfo(card))).is_err() {
self.main_loop.borrow_mut().quit(Retval(0));
}
}
}
fn server_info_cb(self: &Rc<Self>, server_info: &ServerInfo) {
let new_default_sink_name = server_info
.default_sink_name
.as_ref()
.map(|x| x.clone().into_owned());
let mut default_sink_name = self.default_sink_name.borrow_mut();
if new_default_sink_name != *default_sink_name {
if let Some(name) = &new_default_sink_name {
_ = block_on(
self.sender
.borrow_mut()
.send(Event::DefaultSink(name.clone())),
);
self.get_sink_info_by_name(name);
}
*default_sink_name = new_default_sink_name;
}
let new_default_source_name = server_info
.default_source_name
.as_ref()
.map(|x| x.clone().into_owned());
let mut default_source_name = self.default_source_name.borrow_mut();
if new_default_source_name != *default_source_name {
if let Some(name) = &new_default_source_name {
_ = block_on(
self.sender
.borrow_mut()
.send(Event::DefaultSource(name.clone())),
);
self.get_source_info_by_name(name);
}
*default_source_name = new_default_source_name;
}
}
fn get_server_info(self: &Rc<Self>) {
let data = self.clone();
self.introspector
.get_server_info(move |server_info| data.server_info_cb(server_info));
}
fn sink_info_cb(&self, sink_info_res: ListResult<&SinkInfo>) {
if let ListResult::Item(sink_info) = sink_info_res {
if sink_info.name.as_deref() != self.default_sink_name.borrow().as_deref() {
return;
}
let balance = (sink_info.channel_map.can_balance()
&& sink_info.base_volume.is_normal())
.then(|| sink_info.volume.get_balance(&sink_info.channel_map));
let volume = sink_info.volume.max().0 / (Volume::NORMAL.0 / 100);
if self.sink_mute.get() != Some(sink_info.mute) {
self.sink_mute.set(Some(sink_info.mute));
if block_on(
self.sender
.borrow_mut()
.send(Event::SinkMute(sink_info.mute)),
)
.is_err()
{
self.main_loop.borrow_mut().quit(Retval(0));
}
}
if self.sink_volume.get() != Some(volume) {
self.sink_volume.set(Some(volume));
if block_on(self.sender.borrow_mut().send(Event::SinkVolume(volume))).is_err() {
self.main_loop.borrow_mut().quit(Retval(0));
}
}
if block_on(self.sender.borrow_mut().send(Event::Balance(balance))).is_err() {
self.main_loop.borrow_mut().quit(Retval(0));
}
let mut main_loop = self.main_loop.borrow_mut();
let api = main_loop.get_api();
if let Some(mut ctx) = Context::new(&*main_loop, "balance") {
let _ = ctx.connect(None, FlagSet::NOFAIL, None);
let channels = PulseChannels::new(
sink_info.volume,
sink_info.channel_map,
api,
sink_info.index,
ctx,
);
if block_on(self.sender.borrow_mut().send(Event::Channels(channels))).is_err() {
main_loop.quit(Retval(0));
}
}
}
}
fn source_info_cb(&self, source_info_res: ListResult<&SourceInfo>) {
if let ListResult::Item(source_info) = source_info_res {
if source_info.name.as_deref() != self.default_source_name.borrow().as_deref() {
return;
}
let volume = source_info.volume.max().0 / (Volume::NORMAL.0 / 100);
if self.source_mute.get() != Some(source_info.mute) {
self.source_mute.set(Some(source_info.mute));
if block_on(
self.sender
.borrow_mut()
.send(Event::SourceMute(source_info.mute)),
)
.is_err()
{
self.main_loop.borrow_mut().quit(Retval(0));
}
}
if self.source_volume.get() != Some(volume) {
self.source_volume.set(Some(volume));
if block_on(self.sender.borrow_mut().send(Event::SourceVolume(volume))).is_err() {
self.main_loop.borrow_mut().quit(Retval(0));
}
}
}
}
fn get_card_info_by_index(self: &Rc<Self>, index: u32) {
let data = self.clone();
self.introspector
.get_card_info_by_index(index, move |card_info_res| {
data.card_info_cb(card_info_res);
});
}
fn get_sink_info_by_index(self: &Rc<Self>, index: u32) {
let data = self.clone();
self.introspector.get_sink_info_by_index(
index,
move |sink_info_res: ListResult<&SinkInfo<'_>>| {
if let ListResult::Item(ref info) = sink_info_res {
if let Some(card_index) = info.card {
let data_clone = data.clone();
data.introspector.get_card_info_by_index(
card_index,
move |card_info_res| {
data_clone.card_info_cb(card_info_res);
},
);
}
}
data.sink_info_cb(sink_info_res);
},
);
}
fn get_sink_info_by_name(self: &Rc<Self>, name: &str) {
let data = self.clone();
self.introspector
.get_sink_info_by_name(name, move |sink_info_res| {
if let ListResult::Item(ref info) = sink_info_res {
if let Some(card_index) = info.card {
let data_clone = data.clone();
data.introspector.get_card_info_by_index(
card_index,
move |card_info_res| {
data_clone.card_info_cb(card_info_res);
},
);
}
}
data.sink_info_cb(sink_info_res);
});
}
fn get_source_info_by_index(self: &Rc<Self>, index: u32) {
let data = self.clone();
self.introspector
.get_source_info_by_index(index, move |source_info_res| {
if let ListResult::Item(ref info) = source_info_res {
if let Some(card_index) = info.card {
let data_clone = data.clone();
data.introspector.get_card_info_by_index(
card_index,
move |card_info_res| {
data_clone.card_info_cb(card_info_res);
},
);
}
}
data.source_info_cb(source_info_res);
});
}
fn get_source_info_by_name(self: &Rc<Self>, name: &str) {
let data = self.clone();
self.introspector
.get_source_info_by_name(name, move |source_info_res| {
if let ListResult::Item(ref info) = source_info_res {
if let Some(card_index) = info.card {
let data_clone = data.clone();
data.introspector.get_card_info_by_index(
card_index,
move |card_info_res| {
data_clone.card_info_cb(card_info_res);
},
);
}
}
data.source_info_cb(source_info_res);
});
}
fn subscribe_cb(
self: &Rc<Self>,
facility: Facility,
_operation: Option<Operation>,
index: u32,
) {
match facility {
Facility::Server => {
self.get_server_info();
}
Facility::Sink => {
self.get_sink_info_by_index(index);
}
Facility::Source => {
self.get_source_info_by_index(index);
}
Facility::Card => {
self.get_card_info_by_index(index);
}
_ => {}
}
}
}
fn collect_profiles(profiles: &[CardProfileInfo]) -> Vec<CardProfile> {
profiles.iter().map(CardProfile::from).collect()
}
impl From<&CardProfileInfo<'_>> for CardProfile {
fn from(profile: &CardProfileInfo) -> Self {
CardProfile {
name: profile
.name
.as_ref()
.map(Cow::to_string)
.unwrap_or_default(),
description: profile
.description
.as_ref()
.map(Cow::to_string)
.unwrap_or_default(),
available: profile.available,
n_sinks: profile.n_sinks,
n_sources: profile.n_sources,
priority: profile.priority,
}
}
}

View file

@ -3,53 +3,76 @@
// XXX error handling? // XXX error handling?
use std::hash::Hash;
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use iced_futures::Subscription; use iced_futures::Subscription;
use tokio::sync::mpsc::{UnboundedSender, unbounded_channel}; use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
pub(crate) struct Wrapper {
id: &'static str,
conn: zbus::Connection,
}
impl Hash for Wrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
pub fn subscription(connection: zbus::Connection) -> iced_futures::Subscription<Event> { pub fn subscription(connection: zbus::Connection) -> iced_futures::Subscription<Event> {
Subscription::run_with_id( Subscription::run_with(
"settings-daemon", Wrapper {
async move { id: "settings-daemon",
let settings_daemon = match CosmicSettingsDaemonProxy::new(&connection).await { conn: connection,
Ok(value) => value, },
Err(err) => { |Wrapper {
log::error!("Error connecting to settings daemon: {}", err); id: _id,
futures::future::pending().await conn: connection,
} }| {
}; let connection = connection.clone();
async move {
let (tx, rx) = unbounded_channel(); let settings_daemon = match CosmicSettingsDaemonProxy::new(&connection).await {
Ok(value) => value,
let max_brightness_stream = settings_daemon Err(err) => {
.receive_max_display_brightness_changed() log::error!("Error connecting to settings daemon: {}", err);
.await; futures::future::pending().await
let brightness_stream = settings_daemon.receive_display_brightness_changed().await;
let initial = futures::stream::iter([Event::Sender(tx)]);
initial.chain(futures::stream_select!(
Box::pin(UnboundedReceiverStream::new(rx).filter_map(move |request| {
let settings_daemon = settings_daemon.clone();
async move {
match request {
Request::SetDisplayBrightness(brightness) => {
let _ = settings_daemon.set_display_brightness(brightness).await;
}
}
None::<Event>
} }
})), };
Box::pin(max_brightness_stream.filter_map(|evt| async move {
Some(Event::MaxDisplayBrightness(evt.get().await.ok()?)) let (tx, rx) = unbounded_channel();
})),
Box::pin(brightness_stream.filter_map(|evt| async move { let max_brightness_stream = settings_daemon
Some(Event::DisplayBrightness(evt.get().await.ok()?)) .receive_max_display_brightness_changed()
})) .await;
)) let brightness_stream = settings_daemon.receive_display_brightness_changed().await;
}
.flatten_stream(), let initial = futures::stream::iter([Event::Sender(tx)]);
initial.chain(futures::stream_select!(
Box::pin(UnboundedReceiverStream::new(rx).filter_map(move |request| {
let settings_daemon = settings_daemon.clone();
async move {
match request {
Request::SetDisplayBrightness(brightness) => {
let _ =
settings_daemon.set_display_brightness(brightness).await;
}
}
None::<Event>
}
})),
Box::pin(max_brightness_stream.filter_map(|evt| async move {
Some(Event::MaxDisplayBrightness(evt.get().await.ok()?))
})),
Box::pin(brightness_stream.filter_map(|evt| async move {
Some(Event::DisplayBrightness(evt.get().await.ok()?))
}))
))
}
.flatten_stream()
},
) )
} }

View file

@ -19,36 +19,39 @@ pub type ProfileId = i32;
pub type RouteId = u32; pub type RouteId = u32;
pub fn watch() -> impl Stream<Item = Message> + MaybeSend + 'static { pub fn watch() -> impl Stream<Item = Message> + MaybeSend + 'static {
cosmic::iced_futures::stream::channel(1, |mut emitter| async move { cosmic::iced_futures::stream::channel(
loop { 1,
let (cancel_tx, cancel_rx) = futures::channel::oneshot::channel::<()>(); |mut emitter: futures::channel::mpsc::Sender<Message>| async move {
let sender = Arc::new((Mutex::new(Vec::new()), tokio::sync::Notify::const_new())); loop {
let receiver = sender.clone(); let (cancel_tx, cancel_rx) = futures::channel::oneshot::channel::<()>();
let sender = Arc::new((Mutex::new(Vec::new()), tokio::sync::Notify::const_new()));
let receiver = sender.clone();
_ = emitter _ = emitter
.send(Message::SubHandle(Arc::new(SubscriptionHandle { .send(Message::SubHandle(Arc::new(SubscriptionHandle {
cancel_tx, cancel_tx,
pipewire: pipewire::run(move |event| { pipewire: pipewire::run(move |event| {
sender.0.lock().unwrap().push(event); sender.0.lock().unwrap().push(event);
sender.1.notify_one(); sender.1.notify_one();
}), }),
}))) })))
.await; .await;
let forwarder = Box::pin(async { let forwarder = Box::pin(async {
loop { loop {
_ = receiver.1.notified().await; _ = receiver.1.notified().await;
let events = std::mem::take(&mut *receiver.0.lock().unwrap()); let events = std::mem::take(&mut *receiver.0.lock().unwrap());
if !events.is_empty() { if !events.is_empty() {
_ = emitter.send(Message::Server(Arc::from(events))).await; _ = emitter.send(Message::Server(Arc::from(events))).await;
tokio::time::sleep(Duration::from_millis(64)).await; tokio::time::sleep(Duration::from_millis(64)).await;
}
} }
} });
});
futures::future::select(cancel_rx, forwarder).await; futures::future::select(cancel_rx, forwarder).await;
} }
}) },
)
} }
#[derive(Default)] #[derive(Default)]

View file

@ -10,8 +10,7 @@ pub mod device {
pub fn device_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>( pub fn device_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> iced_futures::Subscription<DeviceDbusEvent> { ) -> iced_futures::Subscription<DeviceDbusEvent> {
Subscription::run_with_id( Subscription::run_with(id, |_| {
id,
async move { async move {
match events().await { match events().await {
Ok(stream) => stream, Ok(stream) => stream,
@ -21,8 +20,8 @@ pub mod device {
} }
} }
} }
.flatten_stream(), .flatten_stream()
) })
} }
async fn display_device() -> zbus::Result<(UPowerProxy<'static>, DeviceProxy<'static>)> { async fn display_device() -> zbus::Result<(UPowerProxy<'static>, DeviceProxy<'static>)> {
@ -106,8 +105,7 @@ pub mod kbdbacklight {
pub fn kbd_backlight_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>( pub fn kbd_backlight_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> iced_futures::Subscription<KeyboardBacklightUpdate> { ) -> iced_futures::Subscription<KeyboardBacklightUpdate> {
Subscription::run_with_id( Subscription::run_with(id, |_| {
id,
async move { async move {
match events().await { match events().await {
Ok(stream) => stream, Ok(stream) => stream,
@ -117,8 +115,8 @@ pub mod kbdbacklight {
} }
} }
} }
.flatten_stream(), .flatten_stream()
) })
} }
enum Event { enum Event {