feat: libcosmic iced 0.14 rebase
This commit is contained in:
parent
0020132e63
commit
cf7fc32adf
47 changed files with 2731 additions and 1869 deletions
2636
Cargo.lock
generated
2636
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
24
Cargo.toml
24
Cargo.toml
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = §ion.descriptions;
|
let descriptions = §ion.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)
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -801,46 +801,52 @@ fn keyboard_typing_assist() -> Section<crate::pages::Message> {
|
||||||
|
|
||||||
settings::section()
|
settings::section()
|
||||||
.title(§ion.title)
|
.title(§ion.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)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,7 @@ fn shortcuts() -> Section<crate::pages::Message> {
|
||||||
let descriptions = §ion.descriptions;
|
let descriptions = §ion.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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,15 @@ fn mouse() -> Section<crate::pages::Message> {
|
||||||
|
|
||||||
settings::section()
|
settings::section()
|
||||||
.title(§ion.title)
|
.title(§ion.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(§ion.title)
|
.title(§ion.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])
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(§ion.title)
|
.title(§ion.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(§ion.title)
|
.title(§ion.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 = §ion.descriptions;
|
let descriptions = §ion.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()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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(§ion.title)
|
.title(§ion.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 = §ion.descriptions;
|
let desc = §ion.descriptions;
|
||||||
settings::section()
|
settings::section()
|
||||||
.title(§ion.title)
|
.title(§ion.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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<()> {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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>) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>) {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>) {
|
||||||
|
|
|
||||||
12
subscriptions/pulse/Cargo.toml
Normal file
12
subscriptions/pulse/Cargo.toml
Normal 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"
|
||||||
751
subscriptions/pulse/src/lib.rs
Normal file
751
subscriptions/pulse/src/lib.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue