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"] }
[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"
[workspace.dependencies.cosmic-config]
@ -54,9 +61,9 @@ debug = true
# [patch.'https://github.com/pop-os/cosmic-text']
# cosmic-text = { git = "https://github.com/pop-os/cosmic-text//", rev = "b017d7c" }
# [patch.'https://github.com/pop-os/cosmic-protocols']
# 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" }
[patch.'https://github.com/pop-os/cosmic-protocols']
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" }
# [patch.'https://github.com/pop-os/cosmic-settings-daemon']
# 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-theme = { path = "../libcosmic/cosmic-theme" }
# 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']
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }
# upower_dbus = { path = "../dbus-settings-bindings/upower" }
# 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"
libcosmic.workspace = 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 }
notify = "8.2.0"
regex = "1.12.3"
@ -173,7 +173,13 @@ page-networking = [
"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-users = ["xdg-portal", "dep:accounts-zbus", "dep:zbus", "dep:zbus_polkit"]
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"]
single-instance = ["libcosmic/single-instance"]
test = []
wayland = [
"libcosmic/wayland",
"dep:cosmic-panel-config",
"dep:cosmic-randr"
]
wayland = ["libcosmic/wayland", "dep:cosmic-panel-config", "dep:cosmic-randr"]
wgpu = ["libcosmic/wgpu"]
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(
1,
|mut sender| async move {
|mut sender: futures::channel::mpsc::Sender<crate::pages::Message>| async move {
while let Some(event) = rx.recv().await {
let _ = sender
.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(
1,
|mut sender| async move {
|mut sender: futures::channel::mpsc::Sender<super::Message>| async move {
while let Some(event) = rx.recv().await {
let _ = sender
.send(crate::pages::Message::Accessibility(Message::Event(

View file

@ -10,7 +10,9 @@ use std::{
};
use cosmic::{
Apply, Element, Task, surface,
Apply, Element, Task,
iced::Alignment,
surface,
widget::{self, dropdown, icon, settings},
};
use cosmic_config::{ConfigGet, ConfigSet};
@ -296,6 +298,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "web-browser"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "web-browser"),
@ -313,6 +316,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
.min_item_width(300.0)
}
})
@ -323,6 +327,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "file-manager"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "file-manager"),
@ -340,6 +345,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
@ -349,6 +355,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "mail-client"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "mail-client"),
@ -366,6 +373,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
@ -375,6 +383,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "music"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "music"),
@ -392,6 +401,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
@ -401,6 +411,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "video"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "video"),
@ -418,6 +429,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
@ -427,6 +439,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "photos"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "photos"),
@ -444,6 +457,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
@ -453,6 +467,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "calendar"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "calendar"),
@ -470,6 +485,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
@ -479,6 +495,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "terminal"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "terminal"),
@ -496,6 +513,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
@ -505,6 +523,7 @@ fn apps() -> Section<crate::pages::Message> {
fl!("default-apps", "text-editor"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "text-editor"),
@ -522,6 +541,7 @@ fn apps() -> Section<crate::pages::Message> {
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.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.
let (randr_task, randr_handle) = Task::stream(cosmic::iced_futures::stream::channel(
1,
|mut sender| async move {
|mut sender: futures::channel::mpsc::Sender<_>| async move {
while let Some(message) = rx.recv().await {
if let cosmic_randr::Message::ManagerDone = message
&& !refresh_pending.swap(true, Ordering::SeqCst)

View file

@ -3,7 +3,7 @@
use cosmic::iced::{Alignment, Length, color};
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_settings_bluetooth_subscription::*;
use cosmic_settings_page::{self as page, Section, section};
@ -878,7 +878,7 @@ fn connected_devices() -> Section<crate::pages::Message> {
.wrapping(Wrapping::Word)
.into()
},
widget::horizontal_space().into(),
horizontal_space().into(),
match device.enabled {
Active::Enabled => widget::text(&descriptions[device_connected]).into(),
Active::Enabling => widget::text(&descriptions[device_connecting])
@ -936,18 +936,26 @@ fn available_devices() -> Section<crate::pages::Message> {
let mut items = vec![
widget::icon::from_name(device.icon).size(16).into(),
text(device.alias_or_addr()).wrapping(Wrapping::Word).into(),
widget::horizontal_space().into(),
horizontal_space().into(),
];
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 {
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)
.apply(Element::from)
@ -978,11 +986,9 @@ fn multiple_adapter() -> Section<crate::pages::Message> {
widget::icon::from_name("bluetooth-symbolic")
.size(20)
.into(),
widget::horizontal_space()
.width(theme::spacing().space_xxs)
.into(),
horizontal_space().width(theme::spacing().space_xxs).into(),
text(&adapter.alias).wrapping(Wrapping::Word).into(),
widget::horizontal_space().into(),
horizontal_space().into(),
widget::icon::from_name("go-next-symbolic").into(),
];
if page.model.adapter_connected(path) {

View file

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

View file

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

View file

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

View file

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

View file

@ -25,7 +25,9 @@ use cosmic::{
widget::{
button, dropdown, list_column, row,
segmented_button::{self, SingleSelectModel},
settings, tab_bar, text, toggler,
settings,
space::horizontal as horizontal_space,
tab_bar, text, toggler,
},
};
use cosmic::{
@ -691,7 +693,10 @@ impl Page {
.width(Length::Fixed(SIMULATED_WIDTH as f32))
.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(cosmic::widget::horizontal_space())
.push(horizontal_space())
.push_maybe(add_button)
.into(),
);

View file

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

View file

@ -3,7 +3,7 @@
use cosmic::{
Apply, Element,
iced::Length,
iced::{Alignment, Length},
surface,
widget::{self, settings, toggler},
};
@ -258,12 +258,15 @@ pub fn window_management() -> Section<crate::pages::Message> {
},
),
))
.add(settings::flex_item(
.add(
settings::flex_item(
&descriptions[edge_gravity],
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)
.map(crate::pages::Message::WindowManagement)
})

View file

@ -9,7 +9,7 @@ use cosmic::iced_core::{
Shell, Size, Widget,
};
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_randr_shell::{self as randr, OutputKey};
use randr::Transform;
@ -96,7 +96,7 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
@ -157,17 +157,17 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
layout::Node::new(size)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: cosmic::iced_core::Event,
event: &cosmic::iced_core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let bounds = layout.bounds();
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.offset = (position.x - output_region.x, position.y - output_region.y);
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 {
@ -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(
@ -333,6 +332,7 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
width: 3.0,
},
shadow: Default::default(),
snap: true,
},
core::Background::Color(background.into()),
);
@ -352,6 +352,7 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
..Default::default()
},
shadow: Default::default(),
snap: true,
},
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),
font: cosmic::font::bold(),
bounds: id_bounds.size(),
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
align_x: text::Alignment::Center,
align_y: alignment::Vertical::Center,
shaping: text::Shaping::Basic,
wrapping: text::Wrapping::Word,
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.
let (randr_task, randr_handle) = Task::stream(cosmic::iced_futures::stream::channel(
1,
|mut emitter| async move {
|mut emitter: futures::channel::mpsc::Sender<_>| async move {
while let Some(message) = rx.recv().await {
if let cosmic_randr::Message::ManagerDone = message
&& !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.
let (hotplug_task, hotplug_handle) = Task::stream(cosmic::iced_futures::stream::channel(
1,
|mut emitter| async move {
|mut emitter: futures::channel::mpsc::Sender<pages::Message>| async move {
while let Some(message) = rx.recv().await {
_ = emitter.send(message).await;
}
@ -609,8 +609,8 @@ impl Page {
return cosmic::iced::widget::scrollable::snap_to(
self.display_arrangement_scrollable.clone(),
RelativeOffset {
x: self.last_pan,
y: 0.0,
x: Some(self.last_pan),
y: None,
},
);
}
@ -662,7 +662,10 @@ impl Page {
self.last_pan = 0.5;
cosmic::iced::widget::scrollable::snap_to(
self.display_arrangement_scrollable.clone(),
RelativeOffset { x: 0.5, y: 0.5 },
RelativeOffset {
x: Some(0.5),
y: Some(0.5),
},
)
}

View file

@ -801,7 +801,8 @@ fn keyboard_typing_assist() -> Section<crate::pages::Message> {
settings::section()
.title(&section.title)
.add(settings::flex_item(&descriptions[repeat_delay], {
.add(
settings::flex_item(&descriptions[repeat_delay], {
// Delay
let delay_slider = cosmic::widget::slider(
KB_REPEAT_DELAY_MIN..=KB_REPEAT_DELAY_MAX,
@ -820,8 +821,11 @@ fn keyboard_typing_assist() -> Section<crate::pages::Message> {
.push(widget::text::body(&descriptions[short]))
.push(delay_slider)
.push(widget::text::body(&descriptions[long]))
}))
.add(settings::flex_item(&descriptions[repeat_rate], {
})
.align_items(Alignment::Center),
)
.add(
settings::flex_item(&descriptions[repeat_rate], {
// Repeat rate
let rate_slider = cosmic::widget::slider(
KB_REPEAT_RATE_MIN..=KB_REPEAT_RATE_MAX,
@ -840,7 +844,9 @@ fn keyboard_typing_assist() -> Section<crate::pages::Message> {
.push(widget::text::body(&descriptions[slow]))
.push(rate_slider)
.push(widget::text::body(&descriptions[fast]))
}))
})
.align_items(Alignment::Center),
)
.apply(cosmic::Element::from)
.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)
.flex_control(control)
.align_items(Alignment::Center)
.spacing(16)
.apply(widget::container)
.class(theme::Container::List)

View file

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

View file

@ -62,12 +62,15 @@ fn mouse() -> Section<crate::pages::Message> {
settings::section()
.title(&section.title)
.add(settings::flex_item(
.add(
settings::flex_item(
&descriptions[primary_button],
cosmic::widget::segmented_control::horizontal(&input.primary_button)
.minimum_button_width(0)
.on_activate(|x| Message::PrimaryButtonSelected(x, false)),
))
)
.align_items(Alignment::Center),
)
.add(
settings::item::builder(&descriptions[mouse_speed]).flex_control({
let value = (input
@ -130,7 +133,8 @@ fn scrolling() -> Section<crate::pages::Message> {
settings::section()
.title(&section.title)
.add(settings::flex_item(&descriptions[scroll_speed], {
.add(
settings::flex_item(&descriptions[scroll_speed], {
let value = input
.input_default
.scroll_config
@ -158,7 +162,9 @@ fn scrolling() -> Section<crate::pages::Message> {
.align_x(Alignment::Center),
)
.push(slider)
}))
})
.align_items(Alignment::Center),
)
.add(
settings::item::builder(&descriptions[natural])
.description(&descriptions[natural_desc])

View file

@ -9,12 +9,11 @@ use std::sync::{Arc, LazyLock};
use anyhow::Context;
use cosmic::dialog::file_chooser::FileFilter;
use cosmic::task;
use cosmic::widget::text_input::focus;
use cosmic::{
Apply, Element, Task,
iced::{Alignment, Length},
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::{
@ -1086,7 +1085,7 @@ fn devices_view() -> Section<crate::pages::Message> {
let widget = widget::settings::item_row(vec![
identifier.into(),
widget::horizontal_space().into(),
horizontal_space().into(),
controls.into(),
]);

View file

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

View file

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

View file

@ -5,7 +5,7 @@ use backend::{Battery, ConnectedDevice, PowerProfile};
use cosmic::iced::{self, Alignment, Length};
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::{Task, iced_futures};
use cosmic_config::{Config, CosmicConfigEntry};
@ -15,6 +15,7 @@ use futures::{SinkExt, StreamExt};
use itertools::Itertools;
use slab::Slab;
use slotmap::SlotMap;
use std::hash::Hash;
use std::iter;
use std::time::Duration;
use upower_dbus::DeviceProxy;
@ -153,8 +154,8 @@ impl page::Page<crate::pages::Message> for Page {
});
// Subscriptions for all connected device batteries.
let device_batteries = self
.connected_devices
let device_batteries =
self.connected_devices
.iter()
.filter_map(|device| {
device
@ -163,12 +164,38 @@ impl page::Page<crate::pages::Message> for Page {
.map(|p| (device.device_path.clone(), p))
})
.map(|(path, proxy)| {
iced::Subscription::run_with_id(
path.clone(),
iced_futures::stream::channel(1, |sender| async move {
receive_battery_changes(proxy, path, sender, Message::UpdateDeviceBattery)
#[derive(Clone)]
struct DeviceBatterySubscriptionData {
proxy: DeviceProxy<'static>,
path: String,
}
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
}),
},
)
},
)
});
@ -199,8 +226,11 @@ impl page::Page<crate::pages::Message> for Page {
}
}),
cosmic::Task::run(
iced_futures::stream::channel(1, |mut emitter| async move {
let span = tracing::span!(tracing::Level::INFO, "power::device_stream task");
iced_futures::stream::channel(
1,
|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 {
@ -209,7 +239,8 @@ impl page::Page<crate::pages::Message> for Page {
};
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 added_future = std::pin::pin!(async {
@ -231,15 +262,20 @@ impl page::Page<crate::pages::Message> for Page {
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;
_ = emitter
.send(Message::DeviceDisconnect(device_path))
.await;
}
}
Err(err) => tracing::error!(?err, "cannot establish removed stream"),
Err(err) => {
tracing::error!(?err, "cannot establish removed stream")
}
}
});
futures::future::select(added_future, removed_future).await;
}),
},
),
|msg| msg,
),
];
@ -449,7 +485,7 @@ fn connected_devices() -> Section<crate::pages::Message> {
.width(Length::Fill)
.height(Length::Fill),
)
.height(64)
.height(64.)
.class(cosmic::theme::Container::List)
.into()
})
@ -469,14 +505,10 @@ fn connected_devices() -> Section<crate::pages::Message> {
cosmic::Element::from(
row!(
device_row.next().unwrap_or(
widget::horizontal_space()
.width(Length::Fill)
.into()
horizontal_space().width(Length::Fill).into()
),
device_row.next().unwrap_or(
widget::horizontal_space()
.width(Length::Fill)
.into()
horizontal_space().width(Length::Fill).into()
),
)
.spacing(8),

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ use std::sync::Arc;
use cosmic::app::{ContextDrawer, context_drawer};
use cosmic::iced::{Alignment, Length};
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_config::{ConfigGet, ConfigSet};
use cosmic_settings_page::Section;
@ -398,7 +398,7 @@ impl Page {
.class(cosmic::theme::Svg::Custom(svg_accent.clone()))
.into()
} else {
widget::horizontal_space().width(16).into()
horizontal_space().width(16.).into()
},
])
.apply(widget::container)
@ -526,7 +526,7 @@ impl Page {
.class(cosmic::theme::Svg::Custom(svg_accent.clone()))
.into()
} else {
widget::horizontal_space().width(16).into()
horizontal_space().width(16.).into()
},
])
.apply(widget::container)

View file

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

View file

@ -15,9 +15,10 @@ pub enum Event {
pub fn desktop_files<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> cosmic::iced::Subscription<Event> {
Subscription::run_with_id(
id,
stream::channel(1, move |mut output| async move {
Subscription::run_with(id, |_| {
stream::channel(
1,
move |mut output: futures::channel::mpsc::Sender<Event>| async move {
let handle = tokio::runtime::Handle::current();
let (tx, mut rx) = mpsc::channel(4);
let mut last_update = std::time::Instant::now();
@ -28,7 +29,9 @@ pub fn desktop_files<I: 'static + Hash + Copy + Send + Sync + Debug>(
move |res: Result<notify::Event, notify::Error>| {
if let Ok(event) = res {
match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
EventKind::Create(_)
| EventKind::Modify(_)
| EventKind::Remove(_) => {
let now = std::time::Instant::now();
if now.duration_since(last_update).as_secs() > 3 {
_ = handle.block_on(tx.send(()));
@ -54,6 +57,7 @@ pub fn desktop_files<I: 'static + Hash + Copy + Send + Sync + Debug>(
}
futures::future::pending().await
}),
},
)
})
}

View file

@ -25,15 +25,16 @@ pub enum WallpaperEvent {
}
pub fn wallpapers(current_dir: PathBuf) -> cosmic::iced::Subscription<WallpaperEvent> {
Subscription::run_with_id(
current_dir.clone(),
stream::channel(2, |tx| async {
Subscription::run_with(current_dir, |current_dir: &PathBuf| {
let current_dir = current_dir.clone();
stream::channel(2, move |tx: Sender<WallpaperEvent>| async move {
let current_dir = current_dir.clone();
if let Err(err) = inner(tx, current_dir).await {
tracing::error!("Wallpapers subscription error: {:?}", err);
}
future::pending().await
}),
)
})
})
}
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,
},
shadow: Default::default(),
snap: true,
}
})
}
@ -35,6 +36,7 @@ pub fn display_container_screen() -> cosmic::theme::Container<'static> {
width: 0.0,
},
shadow: Default::default(),
snap: true,
}
})
}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,8 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::Wrapper;
use super::Event;
use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt};
@ -18,13 +20,13 @@ pub fn active_conns_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>
id: I,
conn: Connection,
) -> iced_futures::Subscription<Event> {
Subscription::run_with_id(
id,
Subscription::run_with(Wrapper { id, conn: conn }, |Wrapper { id: _id, conn }| {
let conn = conn.clone();
stream::channel(50, move |output| async move {
watch(conn, output).await;
futures::future::pending().await
}),
)
})
})
}
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,
};
use core::hash;
use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt};
use iced_futures::{self, Subscription, stream};
@ -166,12 +167,35 @@ pub fn subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
has_popup: bool,
conn: Connection,
) -> iced_futures::Subscription<Event> {
Subscription::run_with_id(
(id, has_popup),
struct Wrapper<I> {
id: I,
has_popup: bool,
conn: Connection,
}
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 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;
pub use cosmic_dbus_networkmanager as dbus;
@ -41,6 +41,17 @@ use self::{
pub type SSID = 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)]
pub enum Error {
#[error("failed to list bluetooth devices with rfkill")]
@ -121,13 +132,25 @@ pub fn subscription<I: Copy + Debug + std::hash::Hash + 'static>(
id: I,
conn: zbus::Connection,
) -> iced_futures::Subscription<Event> {
Subscription::run_with_id(
id,
stream::channel(50, |output| async move {
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);
}
}
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>) {

View file

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

View file

@ -1,6 +1,8 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::Wrapper;
use super::Event;
use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt};
@ -18,13 +20,13 @@ pub fn wireless_enabled_subscription<I: 'static + Hash + Copy + Send + Sync + De
id: I,
conn: Connection,
) -> iced_futures::Subscription<Event> {
Subscription::run_with_id(
id,
Subscription::run_with(Wrapper { id, conn: conn }, |Wrapper { id: _id, conn }| {
let conn = conn.clone();
stream::channel(50, move |output| async move {
watch(conn, output).await;
futures::future::pending().await
}),
)
})
})
}
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,14 +3,35 @@
// XXX error handling?
use std::hash::Hash;
use futures::{FutureExt, StreamExt};
use iced_futures::Subscription;
use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
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> {
Subscription::run_with_id(
"settings-daemon",
Subscription::run_with(
Wrapper {
id: "settings-daemon",
conn: connection,
},
|Wrapper {
id: _id,
conn: connection,
}| {
let connection = connection.clone();
async move {
let settings_daemon = match CosmicSettingsDaemonProxy::new(&connection).await {
Ok(value) => value,
@ -35,7 +56,8 @@ pub fn subscription(connection: zbus::Connection) -> iced_futures::Subscription<
async move {
match request {
Request::SetDisplayBrightness(brightness) => {
let _ = settings_daemon.set_display_brightness(brightness).await;
let _ =
settings_daemon.set_display_brightness(brightness).await;
}
}
None::<Event>
@ -49,7 +71,8 @@ pub fn subscription(connection: zbus::Connection) -> iced_futures::Subscription<
}))
))
}
.flatten_stream(),
.flatten_stream()
},
)
}

View file

@ -19,7 +19,9 @@ pub type ProfileId = i32;
pub type RouteId = u32;
pub fn watch() -> impl Stream<Item = Message> + MaybeSend + 'static {
cosmic::iced_futures::stream::channel(1, |mut emitter| async move {
cosmic::iced_futures::stream::channel(
1,
|mut emitter: futures::channel::mpsc::Sender<Message>| async move {
loop {
let (cancel_tx, cancel_rx) = futures::channel::oneshot::channel::<()>();
let sender = Arc::new((Mutex::new(Vec::new()), tokio::sync::Notify::const_new()));
@ -48,7 +50,8 @@ pub fn watch() -> impl Stream<Item = Message> + MaybeSend + 'static {
futures::future::select(cancel_rx, forwarder).await;
}
})
},
)
}
#[derive(Default)]

View file

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