Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17a2f62437 | ||
|
|
4bb0d69ce1 |
186 changed files with 3766 additions and 9942 deletions
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,8 +0,0 @@
|
|||
- [ ] I have disclosed use of any AI generated code in my commit messages.
|
||||
- If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR.
|
||||
- In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment.
|
||||
- [ ] I understand these changes in full and will be able to respond to review comments.
|
||||
- [ ] My change is accurately described in the commit message.
|
||||
- [ ] My contribution is tested and working as described.
|
||||
- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
|
||||
|
||||
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
|
|
@ -33,17 +33,16 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test_args:
|
||||
- --no-default-features --features "" # for cosmic-comp, don't remove!
|
||||
- --no-default-features --features "winit_debug"
|
||||
- --no-default-features --features "winit_tokio"
|
||||
- --no-default-features --features "winit"
|
||||
- --no-default-features --features "winit_wgpu"
|
||||
- --no-default-features --features "wayland"
|
||||
- --no-default-features --features "applet"
|
||||
- --no-default-features --features "desktop,smol"
|
||||
- --no-default-features --features "desktop,tokio"
|
||||
- -p cosmic-theme
|
||||
features:
|
||||
- "" # for cosmic-comp, don't remove!
|
||||
- 'winit_debug'
|
||||
- 'winit_tokio'
|
||||
- winit
|
||||
- winit_wgpu
|
||||
- wayland
|
||||
- applet
|
||||
- desktop,smol
|
||||
- desktop,tokio
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
|
|
@ -67,7 +66,7 @@ jobs:
|
|||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Test features
|
||||
run: cargo test ${{ matrix.test_args }} -- --test-threads=1
|
||||
run: cargo test --no-default-features --features "${{ matrix.features }}"
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
|
|
@ -104,7 +103,7 @@ jobs:
|
|||
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Check example
|
||||
- name: Test example
|
||||
run: cargo check -p "${{ matrix.examples }}"
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
|
|
|||
37
.github/workflows/pages.yml
vendored
37
.github/workflows/pages.yml
vendored
|
|
@ -7,30 +7,19 @@ on:
|
|||
|
||||
jobs:
|
||||
pages:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-07-31
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
|
||||
- name: Build documentation
|
||||
run: |
|
||||
RUSTDOCFLAGS="--cfg docsrs" \
|
||||
cargo +nightly-2025-07-31 doc --no-deps \
|
||||
-p cosmic-client-toolkit \
|
||||
-p cosmic-protocols \
|
||||
-p libcosmic \
|
||||
--verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window
|
||||
- name: Deploy documentation
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./target/doc
|
||||
force_orphan: true
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build documentation
|
||||
run: cargo doc --verbose --features tokio,winit
|
||||
- name: Deploy documentation
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./target/doc
|
||||
force_orphan: true
|
||||
|
|
|
|||
96
Cargo.toml
96
Cargo.toml
|
|
@ -1,36 +1,20 @@
|
|||
[package]
|
||||
name = "libcosmic"
|
||||
version = "1.0.0"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
rust-version = "1.85"
|
||||
|
||||
[lib]
|
||||
name = "cosmic"
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"winit",
|
||||
"tokio",
|
||||
"a11y",
|
||||
"dbus-config",
|
||||
"x11",
|
||||
"iced-wayland",
|
||||
"multi-window",
|
||||
]
|
||||
advanced-shaping = ["iced/advanced-shaping"]
|
||||
default = ["dbus-config", "multi-window", "a11y"]
|
||||
# Accessibility support
|
||||
a11y = ["iced/a11y", "iced_accessibility"]
|
||||
# Enable about widget
|
||||
about = []
|
||||
# Builds support for animated images
|
||||
animated-image = [
|
||||
"dep:async-fs",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/png",
|
||||
"tokio?/io-util",
|
||||
"tokio?/fs",
|
||||
]
|
||||
animated-image = ["dep:async-fs", "image/gif", "tokio?/io-util", "tokio?/fs"]
|
||||
# XXX autosize should not be used on winit windows unless dialogs
|
||||
autosize = []
|
||||
applet = [
|
||||
|
|
@ -58,7 +42,6 @@ desktop = [
|
|||
"process",
|
||||
"dep:cosmic-settings-config",
|
||||
"dep:freedesktop-desktop-entry",
|
||||
"dep:image-extras",
|
||||
"dep:mime",
|
||||
"dep:shlex",
|
||||
"tokio?/io-util",
|
||||
|
|
@ -82,24 +65,18 @@ tokio = [
|
|||
]
|
||||
# Tokio async runtime
|
||||
# Wayland window support
|
||||
iced-wayland = [
|
||||
wayland = [
|
||||
"ashpd?/wayland",
|
||||
"autosize",
|
||||
"iced_runtime/wayland",
|
||||
"iced/wayland",
|
||||
"iced_winit/wayland",
|
||||
"cctk",
|
||||
"surface-message",
|
||||
]
|
||||
wayland = [
|
||||
"iced-wayland",
|
||||
"iced_runtime/cctk",
|
||||
"iced_winit/cctk",
|
||||
"iced_wgpu/cctk",
|
||||
"iced/cctk",
|
||||
"dep:cctk",
|
||||
]
|
||||
surface-message = []
|
||||
# multi-window support
|
||||
multi-window = []
|
||||
multi-window = ["iced/multi-window"]
|
||||
# Render with wgpu
|
||||
wgpu = ["iced/wgpu", "iced_wgpu"]
|
||||
# X11 window support via winit
|
||||
|
|
@ -119,16 +96,15 @@ async-std = [
|
|||
"zbus?/async-io",
|
||||
"iced/async-std",
|
||||
]
|
||||
x11 = ["iced/x11", "iced_winit/x11"]
|
||||
|
||||
[dependencies]
|
||||
apply = "0.3.0"
|
||||
ashpd = { version = "0.12.3", default-features = false, optional = true }
|
||||
async-fs = { version = "2.2", optional = true }
|
||||
ashpd = { version = "0.12.0", default-features = false, optional = true }
|
||||
async-fs = { version = "2.1", optional = true }
|
||||
async-std = { version = "1.13", optional = true }
|
||||
auto_enums = "0.8.8"
|
||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
|
||||
jiff = "0.2"
|
||||
auto_enums = "0.8.7"
|
||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true }
|
||||
chrono = "0.4.42"
|
||||
cosmic-config = { path = "cosmic-config" }
|
||||
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
|
||||
# Internationalization
|
||||
|
|
@ -137,51 +113,45 @@ i18n-embed = { version = "0.16.0", features = [
|
|||
"desktop-requester",
|
||||
] }
|
||||
i18n-embed-fl = "0.10"
|
||||
rust-embed = "8.11.0"
|
||||
rust-embed = "8.7.2"
|
||||
css-color = "0.2.8"
|
||||
derive_setters = "0.1.9"
|
||||
derive_setters = "0.1.8"
|
||||
futures = "0.3"
|
||||
image = { version = "0.25.10", default-features = false, features = [
|
||||
"ico",
|
||||
image = { version = "0.25.8", default-features = false, features = [
|
||||
"jpeg",
|
||||
"png",
|
||||
] }
|
||||
image-extras = { version = "0.1.0", default-features = false, features = [
|
||||
"xpm",
|
||||
"xbm",
|
||||
], optional = true }
|
||||
libc = { version = "0.2.183", optional = true }
|
||||
log = "0.4"
|
||||
libc = { version = "0.2.175", optional = true }
|
||||
mime = { version = "0.3.17", optional = true }
|
||||
palette = "0.7.6"
|
||||
rfd = { version = "0.16.0", default-features = false, features = [
|
||||
raw-window-handle = "0.6"
|
||||
rfd = { version = "0.15.4", default-features = false, features = [
|
||||
"xdg-portal",
|
||||
], optional = true }
|
||||
rustix = { version = "1.1", features = ["pipe", "process"], optional = true }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
slotmap = "1.1.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
slotmap = "1.0.7"
|
||||
smol = { version = "2.0.2", optional = true }
|
||||
thiserror = "2.0.18"
|
||||
taffy = { version = "0.9.2", features = ["grid"] }
|
||||
tokio = { version = "1.50.0", optional = true }
|
||||
tracing = "0.1.44"
|
||||
thiserror = "2.0.16"
|
||||
taffy = { version = "0.9.1", features = ["grid"] }
|
||||
tokio = { version = "1.47.1", optional = true }
|
||||
tracing = "0.1.41"
|
||||
unicode-segmentation = "1.12"
|
||||
url = "2.5.8"
|
||||
zbus = { version = "5.14.0", default-features = false, optional = true }
|
||||
float-cmp = "0.10.0"
|
||||
url = "2.5.7"
|
||||
zbus = { version = "5.11.0", default-features = false, optional = true }
|
||||
|
||||
# Enable DBus feature on Linux targets
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
cosmic-config = { path = "cosmic-config", features = ["dbus"] }
|
||||
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||
zbus = { version = "5.14.0", default-features = false }
|
||||
zbus = { version = "5.11.0", default-features = false }
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" }
|
||||
freedesktop-desktop-entry = { version = "0.8.1", optional = true }
|
||||
freedesktop-desktop-entry = { version = "0.7.14", optional = true }
|
||||
shlex = { version = "1.3.0", optional = true }
|
||||
|
||||
[target.'cfg(any(not(unix), target_os = "macos"))'.dependencies]
|
||||
[target.'cfg(not(unix))'.dependencies]
|
||||
# Used to embed bundled icons for non-unix platforms.
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
|
||||
|
|
@ -238,7 +208,7 @@ git = "https://github.com/pop-os/cosmic-panel"
|
|||
optional = true
|
||||
|
||||
[dependencies.ron]
|
||||
version = "0.12"
|
||||
version = "0.11"
|
||||
optional = true
|
||||
|
||||
[workspace]
|
||||
|
|
@ -254,4 +224,4 @@ exclude = ["iced"]
|
|||
dirs = "6.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.27.0"
|
||||
tempfile = "3.13.0"
|
||||
|
|
|
|||
4
build.rs
4
build.rs
|
|
@ -3,9 +3,7 @@ use std::env;
|
|||
fn main() {
|
||||
println!("cargo::rerun-if-changed=build.rs");
|
||||
|
||||
if env::var_os("CARGO_CFG_UNIX").is_none()
|
||||
|| env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos")
|
||||
{
|
||||
if env::var_os("CARGO_CFG_UNIX").is_none() {
|
||||
generate_bundled_icons();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "cosmic-config-derive"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
|
|||
})
|
||||
});
|
||||
|
||||
let generate = quote! {
|
||||
let gen = quote! {
|
||||
impl CosmicConfigEntry for #name {
|
||||
const VERSION: u64 = #version;
|
||||
|
||||
|
|
@ -147,5 +147,5 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
|
|||
}
|
||||
};
|
||||
|
||||
generate.into()
|
||||
gen.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-config"
|
||||
version = "1.0.0"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
|
|
@ -11,18 +11,18 @@ subscription = ["iced_futures"]
|
|||
|
||||
[dependencies]
|
||||
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
zbus = { version = "5.14.0", default-features = false, optional = true }
|
||||
zbus = { version = "5.11.0", default-features = false, optional = true }
|
||||
atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" }
|
||||
calloop = { version = "0.14.4", optional = true }
|
||||
calloop = { version = "0.14.3", optional = true }
|
||||
notify = "8.2.0"
|
||||
ron = "0.12.0"
|
||||
serde = "1.0.228"
|
||||
ron = "0.11.0"
|
||||
serde = "1.0.219"
|
||||
cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true }
|
||||
iced = { path = "../iced/", default-features = false, optional = true }
|
||||
iced_futures = { path = "../iced/futures/", default-features = false, optional = true }
|
||||
futures-util = { version = "0.3", optional = true }
|
||||
dirs.workspace = true
|
||||
tokio = { version = "1.50", optional = true, features = ["time"] }
|
||||
tokio = { version = "1.47", optional = true, features = ["time"] }
|
||||
async-std = { version = "1.13", optional = true }
|
||||
tracing = "0.1"
|
||||
|
||||
|
|
@ -30,4 +30,4 @@ tracing = "0.1"
|
|||
xdg = "3.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
known-folders = "1.4.2"
|
||||
known-folders = "1.3.1"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::{any::TypeId, ops::Deref};
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::{CosmicConfigEntry, Update};
|
||||
use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy};
|
||||
use futures_util::SinkExt;
|
||||
use iced_futures::{
|
||||
Subscription,
|
||||
futures::{self, StreamExt, future::pending},
|
||||
futures::{self, Stream, StreamExt, future::pending},
|
||||
stream,
|
||||
};
|
||||
|
||||
|
|
@ -57,20 +57,6 @@ impl Watcher {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Wrapper(
|
||||
TypeId,
|
||||
CosmicSettingsDaemonProxy<'static>,
|
||||
&'static str,
|
||||
bool,
|
||||
);
|
||||
|
||||
impl std::hash::Hash for Wrapper {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn watcher_subscription<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone>(
|
||||
settings_daemon: CosmicSettingsDaemonProxy<'static>,
|
||||
|
|
@ -78,185 +64,166 @@ pub fn watcher_subscription<T: CosmicConfigEntry + Send + Sync + Default + 'stat
|
|||
is_state: bool,
|
||||
) -> iced_futures::Subscription<Update<T>> {
|
||||
let id = std::any::TypeId::of::<T>();
|
||||
Subscription::run_with(
|
||||
Wrapper(id, settings_daemon, config_id, is_state),
|
||||
|&Wrapper(_, ref settings_daemon, ref config_id, ref is_state)| {
|
||||
let is_state = *is_state;
|
||||
let config_id = *config_id;
|
||||
let settings_daemon = settings_daemon.clone();
|
||||
enum Change {
|
||||
Changes(Changed),
|
||||
OwnerChanged(bool),
|
||||
}
|
||||
stream::channel(
|
||||
5,
|
||||
move |mut tx: futures::channel::mpsc::Sender<Update<T>>| async move {
|
||||
let version = T::VERSION;
|
||||
|
||||
let Ok(cosmic_config) = (if is_state {
|
||||
crate::Config::new_state(config_id, version)
|
||||
} else {
|
||||
crate::Config::new(config_id, version)
|
||||
}) else {
|
||||
pending::<()>().await;
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
let mut attempts = 0;
|
||||
|
||||
loop {
|
||||
let watcher = if is_state {
|
||||
Watcher::new_state(&settings_daemon, config_id, version).await
|
||||
} else {
|
||||
Watcher::new_config(&settings_daemon, config_id, version).await
|
||||
};
|
||||
let Ok(watcher) = watcher else {
|
||||
tracing::error!("Failed to create watcher for {config_id}");
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
::tokio::time::sleep(::tokio::time::Duration::from_secs(
|
||||
2_u64.pow(attempts),
|
||||
))
|
||||
.await;
|
||||
#[cfg(feature = "async-std")]
|
||||
async_std::task::sleep(std::time::Duration::from_secs(
|
||||
2_u64.pow(attempts),
|
||||
))
|
||||
.await;
|
||||
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
|
||||
{
|
||||
pending::<()>().await;
|
||||
unreachable!();
|
||||
}
|
||||
attempts += 1;
|
||||
// The settings daemon has exited
|
||||
continue;
|
||||
};
|
||||
let Ok(changes) = watcher.receive_changed().await else {
|
||||
tracing::error!("Failed to listen for changes for {config_id}");
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
::tokio::time::sleep(::tokio::time::Duration::from_secs(
|
||||
2_u64.pow(attempts),
|
||||
))
|
||||
.await;
|
||||
#[cfg(feature = "async-std")]
|
||||
async_std::task::sleep(std::time::Duration::from_secs(
|
||||
2_u64.pow(attempts),
|
||||
))
|
||||
.await;
|
||||
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
|
||||
{
|
||||
pending::<()>().await;
|
||||
unreachable!();
|
||||
}
|
||||
attempts += 1;
|
||||
// The settings daemon has exited
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut changes = changes.map(Change::Changes).fuse();
|
||||
|
||||
let Ok(owner_changed) = watcher.inner().receive_owner_changed().await
|
||||
else {
|
||||
tracing::error!("Failed to listen for owner changes for {config_id}");
|
||||
#[cfg(feature = "tokio")]
|
||||
::tokio::time::sleep(::tokio::time::Duration::from_secs(
|
||||
2_u64.pow(attempts),
|
||||
))
|
||||
.await;
|
||||
#[cfg(feature = "async-std")]
|
||||
async_std::task::sleep(std::time::Duration::from_secs(
|
||||
2_u64.pow(attempts),
|
||||
))
|
||||
.await;
|
||||
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
|
||||
{
|
||||
pending::<()>().await;
|
||||
unreachable!();
|
||||
}
|
||||
attempts += 1;
|
||||
// The settings daemon has exited
|
||||
continue;
|
||||
};
|
||||
let mut owner_changed = owner_changed
|
||||
.map(|c| Change::OwnerChanged(c.is_some()))
|
||||
.fuse();
|
||||
|
||||
// update now, just in case we missed changes while setting up stream
|
||||
let mut config = match T::get_entry(&cosmic_config) {
|
||||
Ok(config) => config,
|
||||
Err((errors, default)) => {
|
||||
for why in &errors {
|
||||
if why.is_err() {
|
||||
if let crate::Error::GetKey(_, err) = &why {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
// No system default config installed; don't error
|
||||
continue;
|
||||
}
|
||||
}
|
||||
tracing::error!("error getting config: {config_id} {why}");
|
||||
}
|
||||
}
|
||||
default
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = tx
|
||||
.send(Update {
|
||||
errors: Vec::new(),
|
||||
keys: Vec::new(),
|
||||
config: config.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to send config: {err}");
|
||||
}
|
||||
|
||||
loop {
|
||||
let change: Changed = futures::select! {
|
||||
c = changes.next() => {
|
||||
let Some(Change::Changes(c)) = c else {
|
||||
break;
|
||||
};
|
||||
c
|
||||
}
|
||||
c = owner_changed.next() => {
|
||||
let Some(Change::OwnerChanged(cont)) = c else {
|
||||
break;
|
||||
};
|
||||
if cont {
|
||||
continue;
|
||||
} else {
|
||||
// The settings daemon has exited
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Reset the attempts counter if we received a change
|
||||
attempts = 0;
|
||||
let Ok(args) = change.args() else {
|
||||
// The settings daemon has exited
|
||||
break;
|
||||
};
|
||||
let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]);
|
||||
if !keys.is_empty() {
|
||||
if let Err(err) = tx
|
||||
.send(Update {
|
||||
errors,
|
||||
keys,
|
||||
config: config.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to send config update: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
Subscription::run_with_id(
|
||||
(id, config_id),
|
||||
watcher_stream(settings_daemon, config_id, is_state),
|
||||
)
|
||||
}
|
||||
|
||||
fn watcher_stream<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone>(
|
||||
settings_daemon: CosmicSettingsDaemonProxy<'static>,
|
||||
config_id: &'static str,
|
||||
is_state: bool,
|
||||
) -> impl Stream<Item = Update<T>> {
|
||||
enum Change {
|
||||
Changes(Changed),
|
||||
OwnerChanged(bool),
|
||||
}
|
||||
stream::channel(5, move |mut tx| async move {
|
||||
let version = T::VERSION;
|
||||
|
||||
let Ok(cosmic_config) = (if is_state {
|
||||
crate::Config::new_state(config_id, version)
|
||||
} else {
|
||||
crate::Config::new(config_id, version)
|
||||
}) else {
|
||||
pending::<()>().await;
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
let mut attempts = 0;
|
||||
|
||||
loop {
|
||||
let watcher = if is_state {
|
||||
Watcher::new_state(&settings_daemon, config_id, version).await
|
||||
} else {
|
||||
Watcher::new_config(&settings_daemon, config_id, version).await
|
||||
};
|
||||
let Ok(watcher) = watcher else {
|
||||
tracing::error!("Failed to create watcher for {config_id}");
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await;
|
||||
#[cfg(feature = "async-std")]
|
||||
async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await;
|
||||
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
|
||||
{
|
||||
pending::<()>().await;
|
||||
unreachable!();
|
||||
}
|
||||
attempts += 1;
|
||||
// The settings daemon has exited
|
||||
continue;
|
||||
};
|
||||
let Ok(changes) = watcher.receive_changed().await else {
|
||||
tracing::error!("Failed to listen for changes for {config_id}");
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await;
|
||||
#[cfg(feature = "async-std")]
|
||||
async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await;
|
||||
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
|
||||
{
|
||||
pending::<()>().await;
|
||||
unreachable!();
|
||||
}
|
||||
attempts += 1;
|
||||
// The settings daemon has exited
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut changes = changes.map(Change::Changes).fuse();
|
||||
|
||||
let Ok(owner_changed) = watcher.inner().receive_owner_changed().await else {
|
||||
tracing::error!("Failed to listen for owner changes for {config_id}");
|
||||
#[cfg(feature = "tokio")]
|
||||
::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await;
|
||||
#[cfg(feature = "async-std")]
|
||||
async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await;
|
||||
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
|
||||
{
|
||||
pending::<()>().await;
|
||||
unreachable!();
|
||||
}
|
||||
attempts += 1;
|
||||
// The settings daemon has exited
|
||||
continue;
|
||||
};
|
||||
let mut owner_changed = owner_changed
|
||||
.map(|c| Change::OwnerChanged(c.is_some()))
|
||||
.fuse();
|
||||
|
||||
// update now, just in case we missed changes while setting up stream
|
||||
let mut config = match T::get_entry(&cosmic_config) {
|
||||
Ok(config) => config,
|
||||
Err((errors, default)) => {
|
||||
for why in &errors {
|
||||
if why.is_err() {
|
||||
if let crate::Error::GetKey(_, err) = &why {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
// No system default config installed; don't error
|
||||
continue;
|
||||
}
|
||||
}
|
||||
tracing::error!("error getting config: {config_id} {why}");
|
||||
}
|
||||
}
|
||||
default
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = tx
|
||||
.send(Update {
|
||||
errors: Vec::new(),
|
||||
keys: Vec::new(),
|
||||
config: config.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to send config: {err}");
|
||||
}
|
||||
|
||||
loop {
|
||||
let change: Changed = futures::select! {
|
||||
c = changes.next() => {
|
||||
let Some(Change::Changes(c)) = c else {
|
||||
break;
|
||||
};
|
||||
c
|
||||
}
|
||||
c = owner_changed.next() => {
|
||||
let Some(Change::OwnerChanged(cont)) = c else {
|
||||
break;
|
||||
};
|
||||
if cont {
|
||||
continue;
|
||||
} else {
|
||||
// The settings daemon has exited
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Reset the attempts counter if we received a change
|
||||
attempts = 0;
|
||||
let Ok(args) = change.args() else {
|
||||
// The settings daemon has exited
|
||||
break;
|
||||
};
|
||||
let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]);
|
||||
if !keys.is_empty() {
|
||||
if let Err(err) = tx
|
||||
.send(Update {
|
||||
errors,
|
||||
keys,
|
||||
config: config.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to send config update: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,24 +25,7 @@ pub fn config_subscription<
|
|||
config_id: Cow<'static, str>,
|
||||
config_version: u64,
|
||||
) -> iced_futures::Subscription<crate::Update<T>> {
|
||||
iced_futures::Subscription::run_with(
|
||||
(id, config_id, config_version, false),
|
||||
// FIXME there are type issues related to the 'static lifetime of the Cow if this is extracted to a named function...
|
||||
|(_, config_id, config_version, is_state)| {
|
||||
let config_id = config_id.clone();
|
||||
let config_version = *config_version;
|
||||
let is_state = *is_state;
|
||||
|
||||
stream::channel(100, move |mut output| async move {
|
||||
let config_id = config_id.clone();
|
||||
let mut state = ConfigState::Init(config_id, config_version, is_state);
|
||||
|
||||
loop {
|
||||
state = start_listening::<T>(state, &mut output).await;
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, false))
|
||||
}
|
||||
|
||||
#[cold]
|
||||
|
|
@ -54,23 +37,25 @@ pub fn config_state_subscription<
|
|||
config_id: Cow<'static, str>,
|
||||
config_version: u64,
|
||||
) -> iced_futures::Subscription<crate::Update<T>> {
|
||||
iced_futures::Subscription::run_with(
|
||||
(id, config_id, config_version, true),
|
||||
|(_, config_id, config_version, is_state)| {
|
||||
iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, true))
|
||||
}
|
||||
|
||||
fn watcher_stream<T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry>(
|
||||
config_id: Cow<'static, str>,
|
||||
config_version: u64,
|
||||
is_state: bool,
|
||||
) -> impl Stream<Item = crate::Update<T>> {
|
||||
stream::channel(100, move |mut output| {
|
||||
let config_id = config_id.clone();
|
||||
async move {
|
||||
let config_id = config_id.clone();
|
||||
let config_version = *config_version;
|
||||
let is_state = *is_state;
|
||||
let mut state = ConfigState::Init(config_id, config_version, is_state);
|
||||
|
||||
stream::channel(100, move |mut output| async move {
|
||||
let config_id = config_id.clone();
|
||||
let mut state = ConfigState::Init(config_id, config_version, is_state);
|
||||
|
||||
loop {
|
||||
state = start_listening::<T>(state, &mut output).await;
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
loop {
|
||||
state = start_listening::<T>(state, &mut output).await;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn start_listening<T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry>(
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 5252095787cc96e2aed64604158f94e450703455
|
||||
Subproject commit 70b07582e24ec2114672256b9657ca80670bca8a
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-theme"
|
||||
version = "1.0.0"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
|
@ -17,23 +17,15 @@ no-default = []
|
|||
[dependencies]
|
||||
palette = { version = "0.7.6", features = ["serializing"] }
|
||||
almost = "0.2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = { version = "1.0.149", optional = true, features = [
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = { version = "1.0.143", optional = true, features = [
|
||||
"preserve_order",
|
||||
] }
|
||||
ron = "0.12.0"
|
||||
csscolorparser = { version = "0.8.3", features = ["serde"] }
|
||||
ron = "0.11.0"
|
||||
csscolorparser = { version = "0.7.2", features = ["serde"] }
|
||||
cosmic-config = { path = "../cosmic-config/", default-features = false, features = [
|
||||
"subscription",
|
||||
"macro",
|
||||
] }
|
||||
configparser = "3.1.0"
|
||||
dirs.workspace = true
|
||||
thiserror = "2.0.18"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.47.2"
|
||||
|
||||
[profile.dev.package]
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
thiserror = "2.0.16"
|
||||
|
|
|
|||
|
|
@ -685,17 +685,18 @@ impl Theme {
|
|||
self.shade
|
||||
}
|
||||
|
||||
/// Get the active theme based on the current theme mode.
|
||||
/// get the active theme
|
||||
pub fn get_active() -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
|
||||
(|| {
|
||||
(if ThemeMode::is_dark(&Config::new(Self::id(), Self::VERSION)?)? {
|
||||
Self::dark_config
|
||||
} else {
|
||||
Self::light_config
|
||||
})()
|
||||
})()
|
||||
.map_err(|error| (vec![error], Self::default()))
|
||||
.and_then(|theme_config| Self::get_entry(&theme_config))
|
||||
let config =
|
||||
Config::new(Self::id(), Self::VERSION).map_err(|e| (vec![e], Self::default()))?;
|
||||
let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?;
|
||||
let config = if is_dark {
|
||||
Self::dark_config()
|
||||
} else {
|
||||
Self::light_config()
|
||||
}
|
||||
.map_err(|e| (vec![e], Self::default()))?;
|
||||
Self::get_entry(&config)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -986,19 +987,19 @@ impl ThemeBuilder {
|
|||
let success = if let Some(success) = success {
|
||||
success.into_color()
|
||||
} else {
|
||||
palette.as_ref().bright_green
|
||||
palette.as_ref().accent_green
|
||||
};
|
||||
|
||||
let warning = if let Some(warning) = warning {
|
||||
warning.into_color()
|
||||
} else {
|
||||
palette.as_ref().bright_orange
|
||||
palette.as_ref().accent_yellow
|
||||
};
|
||||
|
||||
let destructive = if let Some(destructive) = destructive {
|
||||
destructive.into_color()
|
||||
} else {
|
||||
palette.as_ref().bright_red
|
||||
palette.as_ref().accent_red
|
||||
};
|
||||
|
||||
let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap()));
|
||||
|
|
|
|||
|
|
@ -163,19 +163,9 @@ impl Theme {
|
|||
std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
|
||||
}
|
||||
|
||||
let file_path = config_dir.join(name);
|
||||
let tmp_file_path = config_dir.join(name.to_owned() + "~");
|
||||
|
||||
// Write to tmp_file_path first, then move it to file_path
|
||||
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
|
||||
let res = tmp_file
|
||||
.write_all(css_str.as_bytes())
|
||||
.and_then(|_| tmp_file.flush())
|
||||
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
|
||||
if let Err(e) = res {
|
||||
_ = std::fs::remove_file(&tmp_file_path);
|
||||
return Err(OutputError::Io(e));
|
||||
}
|
||||
let mut file = File::create(config_dir.join(name)).map_err(OutputError::Io)?;
|
||||
file.write_all(css_str.as_bytes())
|
||||
.map_err(OutputError::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use configparser::ini::WriteOptions;
|
||||
use palette::{Srgba, rgb::Rgba};
|
||||
use thiserror::Error;
|
||||
|
||||
|
|
@ -7,11 +6,6 @@ use crate::Theme;
|
|||
/// Module for outputting the Cosmic gtk4 theme type as CSS
|
||||
pub mod gtk4_output;
|
||||
|
||||
/// Module for configuring qt5ct and qt6ct to use our qt theme
|
||||
pub mod qt56ct_output;
|
||||
/// Module for outputting the Cosmic qt theme type as kdeglobals
|
||||
pub mod qt_output;
|
||||
|
||||
pub mod vs_code;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
|
@ -20,48 +14,33 @@ pub enum OutputError {
|
|||
Io(std::io::Error),
|
||||
#[error("Missing config directory")]
|
||||
MissingConfigDir,
|
||||
#[error("Missing data directory")]
|
||||
MissingDataDir,
|
||||
#[error("Serde Error: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
#[error("Ini Error: {0}")]
|
||||
Ini(String),
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
#[inline]
|
||||
/// Apply COSMIC theme exports for GTK and Qt applications.
|
||||
pub fn apply_exports(&self) -> Result<(), OutputError> {
|
||||
let gtk_res = Theme::apply_gtk(self.is_dark);
|
||||
let qt_res = Theme::apply_qt(self.is_dark);
|
||||
let qt56ct_res = Theme::apply_qt56ct(self.is_dark);
|
||||
let vs_res = self.clone().apply_vs_code();
|
||||
gtk_res?;
|
||||
qt_res?;
|
||||
qt56ct_res?;
|
||||
vs_res?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Write COSMIC theme exports for GTK and Qt applications.
|
||||
pub fn write_exports(&self) -> Result<(), OutputError> {
|
||||
let gtk_res = self.write_gtk4();
|
||||
let qt_res = self.write_qt();
|
||||
let qt56ct_res = self.write_qt56ct();
|
||||
gtk_res?;
|
||||
qt_res?;
|
||||
qt56ct_res?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Un-export GTK and Qt theme configurations applied by us.
|
||||
pub fn reset_exports() -> Result<(), OutputError> {
|
||||
let gtk_res = Theme::reset_gtk();
|
||||
let qt_res = Theme::reset_qt();
|
||||
let qt56ct_res = Theme::reset_qt56ct();
|
||||
let vs_res = Theme::reset_vs_code();
|
||||
gtk_res?;
|
||||
qt_res?;
|
||||
qt56ct_res?;
|
||||
vs_res?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -81,9 +60,3 @@ pub fn to_rgba(c: Srgba) -> String {
|
|||
c_u8.red, c_u8.green, c_u8.blue, c.alpha
|
||||
)
|
||||
}
|
||||
|
||||
pub fn qt_settings_ini_style() -> WriteOptions {
|
||||
let mut write_options = WriteOptions::default();
|
||||
write_options.blank_lines_between_sections = 1;
|
||||
write_options
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,415 +0,0 @@
|
|||
use crate::Theme;
|
||||
use configparser::ini::Ini;
|
||||
use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
vec,
|
||||
};
|
||||
|
||||
use super::{OutputError, qt_settings_ini_style};
|
||||
|
||||
impl Theme {
|
||||
/// The "version" of this theme.
|
||||
///
|
||||
/// To avoid repeatedly overwriting the user's config, we use a version system.
|
||||
///
|
||||
/// Increment this value when changes to qt{5,6}ct.conf are needed.
|
||||
/// If the config's version is outdated, we update several sections.
|
||||
/// Otherwise, only the light/dark mode is updated.
|
||||
const COSMIC_QT_VERSION: u64 = 2;
|
||||
|
||||
/// Produces a QPalette ini file for qt5ct and qt6ct.
|
||||
///
|
||||
/// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf
|
||||
#[must_use]
|
||||
#[cold]
|
||||
pub fn as_qpalette(&self) -> String {
|
||||
let lightest = if self.is_dark {
|
||||
self.background.on
|
||||
} else {
|
||||
self.background.base
|
||||
};
|
||||
let darkest = if self.is_dark {
|
||||
self.background.base
|
||||
} else {
|
||||
self.background.on
|
||||
};
|
||||
let active = QPaletteGroup {
|
||||
window_text: self.background.on,
|
||||
button: self.button.base,
|
||||
light: self.button.base.mix(lightest, 0.1),
|
||||
midlight: self.button.base.mix(lightest, 0.05),
|
||||
dark: self.button.base.mix(darkest, 0.1),
|
||||
mid: self.button.base.mix(darkest, 0.05),
|
||||
text: self.background.component.on,
|
||||
bright_text: lightest,
|
||||
button_text: self.button.on,
|
||||
base: self.background.component.base,
|
||||
window: self.background.base,
|
||||
shadow: darkest,
|
||||
// selection colors are swapped to fix menu bar contrast
|
||||
highlight: self.background.component.selected_text,
|
||||
highlighted_text: self.background.component.selected,
|
||||
link: self.link_button.on,
|
||||
link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2),
|
||||
alternate_base: self.background.base.mix(self.accent.base, 0.05),
|
||||
no_role: self.background.component.disabled,
|
||||
tool_tip_base: self.background.component.base,
|
||||
tool_tip_text: self.background.component.on,
|
||||
placeholder_text: self.background.component.on.with_alpha(0.5),
|
||||
};
|
||||
let inactive = QPaletteGroup {
|
||||
window_text: active.window_text.with_alpha(0.8),
|
||||
text: active.text.with_alpha(0.8),
|
||||
highlighted_text: active.highlighted_text.with_alpha(0.8),
|
||||
tool_tip_text: active.tool_tip_text.with_alpha(0.8),
|
||||
..active
|
||||
};
|
||||
let disabled = QPaletteGroup {
|
||||
button: self.button.disabled,
|
||||
text: self.background.component.on_disabled,
|
||||
button_text: self.button.on_disabled,
|
||||
base: self.background.component.disabled,
|
||||
highlighted_text: active.highlighted_text.with_alpha(0.5),
|
||||
link: self.link_button.on_disabled,
|
||||
link_visited: self
|
||||
.link_button
|
||||
.on_disabled
|
||||
.mix(self.secondary.component.disabled, 0.2),
|
||||
alternate_base: self.background.base.mix(self.accent.disabled, 0.05),
|
||||
tool_tip_base: self.background.component.disabled,
|
||||
tool_tip_text: self.background.component.on_disabled,
|
||||
placeholder_text: self.background.component.on_disabled.with_alpha(0.5),
|
||||
..inactive
|
||||
};
|
||||
|
||||
format!(
|
||||
r#"# GENERATED BY COSMIC
|
||||
|
||||
[ColorScheme]
|
||||
active_colors={}
|
||||
disabled_colors={}
|
||||
inactive_colors={}
|
||||
"#,
|
||||
active.as_list(),
|
||||
disabled.as_list(),
|
||||
inactive.as_list(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Writes the QPalette ini files to:
|
||||
/// - `~/.config/qt6ct/colors/`
|
||||
/// - `~/.config/qt5ct/colors/`
|
||||
#[cold]
|
||||
pub fn write_qt56ct(&self) -> Result<(), OutputError> {
|
||||
let qpalette = self.as_qpalette();
|
||||
let qt5ct_res = self.write_ct("qt5ct", &qpalette);
|
||||
let qt6ct_res = self.write_ct("qt6ct", &qpalette);
|
||||
qt5ct_res?;
|
||||
qt6ct_res?;
|
||||
Ok(())
|
||||
}
|
||||
#[must_use]
|
||||
#[cold]
|
||||
fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> {
|
||||
let file_path = Self::get_qpalette_path(ct, self.is_dark)?;
|
||||
let tmp_file_path = file_path.with_extension("conf.new");
|
||||
|
||||
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
|
||||
let res = tmp_file
|
||||
.write_all(qpalette.as_bytes())
|
||||
.and_then(|_| tmp_file.flush())
|
||||
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
|
||||
if let Err(e) = res {
|
||||
_ = std::fs::remove_file(&tmp_file_path);
|
||||
return Err(OutputError::Io(e));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Edits qt{5,6}ct.conf to use COSMIC styles if needed.
|
||||
#[cold]
|
||||
pub fn apply_qt56ct(is_dark: bool) -> Result<(), OutputError> {
|
||||
let qt5ct_res = Self::apply_ct("qt5ct", is_dark);
|
||||
let qt6ct_res = Self::apply_ct("qt6ct", is_dark);
|
||||
qt5ct_res?;
|
||||
qt6ct_res?;
|
||||
Ok(())
|
||||
}
|
||||
#[must_use]
|
||||
#[cold]
|
||||
fn apply_ct(ct: &str, is_dark: bool) -> Result<(), OutputError> {
|
||||
let path = Self::get_conf_path(ct)?;
|
||||
let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?;
|
||||
let mut ini = Ini::new_cs();
|
||||
ini.read(file_content).map_err(OutputError::Ini)?;
|
||||
|
||||
let old_version = ini
|
||||
.getuint("Appearance", "cosmic_qt_version")
|
||||
.map_err(OutputError::Ini)?
|
||||
.unwrap_or_default();
|
||||
|
||||
let color_scheme_path = Self::get_qpalette_path(ct, is_dark)?;
|
||||
let icon_theme = if is_dark { "breeze-dark" } else { "breeze" };
|
||||
|
||||
ini.set(
|
||||
"Appearance",
|
||||
"cosmic_qt_version",
|
||||
Some(Theme::COSMIC_QT_VERSION.to_string()),
|
||||
);
|
||||
|
||||
if old_version < Theme::COSMIC_QT_VERSION {
|
||||
// Config is outdated, update it unconditionally!
|
||||
|
||||
ini.setstr(
|
||||
"Appearance",
|
||||
"color_scheme_path",
|
||||
color_scheme_path.to_str(),
|
||||
);
|
||||
// Enable the above color scheme, instead of using the default color scheme of e.g. Breeze
|
||||
ini.setstr("Appearance", "custom_palette", Some("true"));
|
||||
// COSMIC icons are stuck in light mode, so use breeze icons instead
|
||||
ini.setstr("Appearance", "icon_theme", Some(icon_theme));
|
||||
// Use COSMIC dialogs instead of KDE's
|
||||
ini.setstr("Appearance", "standard_dialogs", Some("xdgdesktopportal"));
|
||||
|
||||
// TODO: Add fonts section to match COSMIC
|
||||
} else {
|
||||
// Config is not outdated, check before updating light/dark mode only!
|
||||
|
||||
let old_color_scheme_path = ini
|
||||
.get("Appearance", "color_scheme_path")
|
||||
.unwrap_or_else(|| "CosmicPlease".to_owned());
|
||||
if old_color_scheme_path.contains("Cosmic") {
|
||||
ini.setstr(
|
||||
"Appearance",
|
||||
"color_scheme_path",
|
||||
color_scheme_path.to_str(),
|
||||
);
|
||||
}
|
||||
|
||||
let old_icon_theme = ini
|
||||
.get("Appearance", "icon_theme")
|
||||
.unwrap_or_else(|| "breeze".to_owned());
|
||||
if old_icon_theme.contains("breeze") {
|
||||
ini.setstr("Appearance", "icon_theme", Some(icon_theme));
|
||||
}
|
||||
}
|
||||
|
||||
ini.pretty_write(path, &qt_settings_ini_style())
|
||||
.map_err(OutputError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file.
|
||||
#[cold]
|
||||
pub fn reset_qt56ct() -> Result<(), OutputError> {
|
||||
let qt5ct_res = Self::reset_ct("qt5ct");
|
||||
let qt6ct_res = Self::reset_ct("qt6ct");
|
||||
qt5ct_res?;
|
||||
qt6ct_res?;
|
||||
Ok(())
|
||||
}
|
||||
#[must_use]
|
||||
#[cold]
|
||||
fn reset_ct(ct: &str) -> Result<(), OutputError> {
|
||||
let path = Self::get_conf_path(ct)?;
|
||||
let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?;
|
||||
let mut ini = Ini::new_cs();
|
||||
ini.read(file_content).map_err(OutputError::Ini)?;
|
||||
|
||||
let old_version = ini
|
||||
.getuint("Appearance", "cosmic_qt_version")
|
||||
.map_err(OutputError::Ini)?
|
||||
.unwrap_or_default();
|
||||
if old_version == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ini.remove_key("Appearance", "cosmic_qt_version");
|
||||
ini.remove_key("Appearance", "color_scheme_path");
|
||||
ini.remove_key("Appearance", "icon_theme");
|
||||
|
||||
ini.pretty_write(path, &qt_settings_ini_style())
|
||||
.map_err(OutputError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the file paths of the form `~/.config/ct/ct.conf`:
|
||||
/// e.g. `~/.config/qt6ct/qt6ct.conf`.
|
||||
///
|
||||
/// The file and its parent directory are created if they don't exist.
|
||||
#[cold]
|
||||
fn get_conf_path(ct: &str) -> Result<PathBuf, OutputError> {
|
||||
assert!(ct == "qt5ct" || ct == "qt6ct");
|
||||
|
||||
let Some(mut config_dir) = dirs::config_dir() else {
|
||||
return Err(OutputError::MissingConfigDir);
|
||||
};
|
||||
config_dir.push(&ct);
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
|
||||
}
|
||||
|
||||
let file_path = config_dir.join(ct.to_owned() + ".conf");
|
||||
if !file_path.exists() {
|
||||
File::create_new(&file_path).map_err(OutputError::Io)?;
|
||||
}
|
||||
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
/// Gets a path like `~/.config/qt6ct/colors/CosmicDark.conf`
|
||||
///
|
||||
/// Its parent directory is created if it doesn't exist.
|
||||
#[cold]
|
||||
fn get_qpalette_path(ct: &str, is_dark: bool) -> Result<PathBuf, OutputError> {
|
||||
assert!(ct == "qt5ct" || ct == "qt6ct");
|
||||
|
||||
let Some(mut config_dir) = dirs::config_dir() else {
|
||||
return Err(OutputError::MissingConfigDir);
|
||||
};
|
||||
config_dir.push(&ct);
|
||||
config_dir.push("colors");
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
|
||||
}
|
||||
|
||||
let file_name = if is_dark {
|
||||
"CosmicDark.conf"
|
||||
} else {
|
||||
"CosmicLight.conf"
|
||||
};
|
||||
|
||||
Ok(config_dir.join(file_name))
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the different symbolic color roles used in current GUIs.
|
||||
///
|
||||
/// qt5ct and qt6ct consume this as a list of colors, ordered by ColorRole:
|
||||
/// - https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum
|
||||
/// - https://doc.qt.io/archives/qt-5.15/qpalette.html#ColorRole-enum
|
||||
struct QPaletteGroup {
|
||||
/// A general foreground color.
|
||||
window_text: Srgba,
|
||||
/// The general button background color.
|
||||
button: Srgba,
|
||||
/// Lighter than [button] color, used mostly for 3D bevel and shadow effects.
|
||||
light: Srgba,
|
||||
/// Between [button] and [light], used mostly for 3D bevel and shadow effects.
|
||||
midlight: Srgba,
|
||||
/// Darker than [button], used mostly for 3D bevel and shadow effects.
|
||||
dark: Srgba,
|
||||
/// Between [button] and [dark], used mostly for 3D bevel and shadow effects.
|
||||
mid: Srgba,
|
||||
/// The foreground color used with [base].
|
||||
text: Srgba,
|
||||
/// A text color that is very different from [window_text], and contrasts well with e.g. [dark].
|
||||
/// Typically used for text that needs to be drawn where [text] or [window_text] would give poor contrast, such as on pressed push buttons.
|
||||
bright_text: Srgba,
|
||||
/// A foreground color used with the [button] color.
|
||||
button_text: Srgba,
|
||||
/// Used mostly as the background color for text entry widgets, but can also be used for other painting -
|
||||
/// such as the background of combobox drop down lists and toolbar handles.
|
||||
base: Srgba,
|
||||
/// A general background color.
|
||||
window: Srgba,
|
||||
/// A very dark color, used mostly for 3D bevel and shadow effects.
|
||||
/// Opaque black by default.
|
||||
shadow: Srgba,
|
||||
/// A color to indicate a selected item or the current item.
|
||||
highlight: Srgba,
|
||||
/// A text color that contrasts with [highlight].
|
||||
highlighted_text: Srgba,
|
||||
/// A text color used for unvisited hyperlinks.
|
||||
link: Srgba,
|
||||
/// A text color used for already visited hyperlinks.
|
||||
link_visited: Srgba,
|
||||
/// Used as the alternate background color in views with alternating row colors.
|
||||
alternate_base: Srgba,
|
||||
/// No role; this special role is often used to indicate that a role has not been assigned.
|
||||
no_role: Srgba,
|
||||
/// Used as the background color for QToolTip and QWhatsThis.
|
||||
/// Tool tips use the inactive color group of QPalette, because tool tips are not active windows.
|
||||
tool_tip_base: Srgba,
|
||||
/// Used as the foreground color for QToolTip and QWhatsThis.
|
||||
/// Tool tips use the inactive color group of QPalette, because tool tips are not active windows.
|
||||
tool_tip_text: Srgba,
|
||||
/// Used as the placeholder color for various text input widgets.
|
||||
placeholder_text: Srgba,
|
||||
// /// [accent] only exists since Qt 6.6. Including it here breaks qt5ct.
|
||||
// /// When omitted, it defaults to [highlight].
|
||||
// accent: Srgba,
|
||||
}
|
||||
|
||||
impl QPaletteGroup {
|
||||
/// Returns a comma-separated list of the colors as hex codes.
|
||||
/// E.g. `#ff000000, #ffdcdcdc, ...`
|
||||
///
|
||||
/// Any transparent colors are flattened with [base] to avoid issues with
|
||||
/// the Fusion style.
|
||||
fn as_list(&self) -> String {
|
||||
let colors = vec![
|
||||
to_argb_hex(self.window_text.over(self.base)),
|
||||
to_argb_hex(self.button.over(self.base)),
|
||||
to_argb_hex(self.light.over(self.base)),
|
||||
to_argb_hex(self.midlight.over(self.base)),
|
||||
to_argb_hex(self.dark.over(self.base)),
|
||||
to_argb_hex(self.mid.over(self.base)),
|
||||
to_argb_hex(self.text.over(self.base)),
|
||||
to_argb_hex(self.bright_text.over(self.base)),
|
||||
to_argb_hex(self.button_text.over(self.base)),
|
||||
to_argb_hex(self.base.over(self.base)),
|
||||
to_argb_hex(self.window.over(self.base)),
|
||||
to_argb_hex(self.shadow.over(self.base)),
|
||||
to_argb_hex(self.highlight.over(self.base)),
|
||||
to_argb_hex(self.highlighted_text.over(self.base)),
|
||||
to_argb_hex(self.link.over(self.base)),
|
||||
to_argb_hex(self.link_visited.over(self.base)),
|
||||
to_argb_hex(self.alternate_base.over(self.base)),
|
||||
to_argb_hex(self.no_role.over(self.base)),
|
||||
to_argb_hex(self.tool_tip_base.over(self.base)),
|
||||
to_argb_hex(self.tool_tip_text.over(self.base)),
|
||||
to_argb_hex(self.placeholder_text.over(self.base)),
|
||||
];
|
||||
colors.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a color to a hex string in the format `#AARRGGBB`.
|
||||
/// Do not use [to_hex] since that uses the format `RRGGBBAA`.
|
||||
fn to_argb_hex(c: Srgba) -> String {
|
||||
let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format();
|
||||
format!(
|
||||
"#{:02x}{:02x}{:02x}{:02x}",
|
||||
c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_color_to_argb_hex() {
|
||||
let color = Srgba::new(0x33, 0x55, 0x77, 0xff);
|
||||
let argb = to_argb_hex(color.into());
|
||||
assert_eq!(argb, "#ff335577");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_light_default_qpalette() {
|
||||
let light_default_qpalette = Theme::light_default().as_qpalette();
|
||||
insta::assert_snapshot!(light_default_qpalette);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dark_default_qpalette() {
|
||||
let dark_default_qpalette = Theme::dark_default().as_qpalette();
|
||||
insta::assert_snapshot!(dark_default_qpalette);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,568 +0,0 @@
|
|||
use crate::Theme;
|
||||
use configparser::ini::Ini;
|
||||
use cosmic_config::CosmicConfigEntry;
|
||||
use palette::{Mix, Srgba, blend::Compose};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use super::{OutputError, qt_settings_ini_style};
|
||||
|
||||
impl Theme {
|
||||
/// Produces a color scheme ini file for Qt.
|
||||
///
|
||||
/// Some high-level documentation for this file can be found at:
|
||||
/// - https://api.kde.org/kcolorscheme.html
|
||||
/// - https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/
|
||||
#[must_use]
|
||||
#[cold]
|
||||
pub fn as_kcolorscheme(&self) -> String {
|
||||
// Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter
|
||||
let disabled_color_effects = IniColorEffects {
|
||||
color: self.button.disabled,
|
||||
color_amount: 0.0,
|
||||
color_effect: ColorEffect::Desaturate,
|
||||
contrast_amount: 0.65,
|
||||
contrast_effect: ColorEffect::Fade,
|
||||
intensity_amount: 0.1,
|
||||
intensity_effect: IntensityEffect::Lighten,
|
||||
};
|
||||
// Usually, inactive elements will have reduced contrast (text fades slightly into the background) and may have slightly reduced intensity
|
||||
let inactive_color_effects = IniColorEffects {
|
||||
color: self.palette.gray_1,
|
||||
color_amount: 0.025,
|
||||
color_effect: ColorEffect::Tint,
|
||||
contrast_amount: 0.1,
|
||||
contrast_effect: ColorEffect::Tint,
|
||||
intensity_amount: 0.0,
|
||||
intensity_effect: IntensityEffect::Shade,
|
||||
};
|
||||
|
||||
let bg = self.background.base;
|
||||
// the background container
|
||||
let window_colors = IniColors {
|
||||
background_alternate: bg.mix(self.accent.base, 0.05),
|
||||
background_normal: bg,
|
||||
decoration_focus: self.accent_text_color(),
|
||||
decoration_hover: self.accent_text_color(),
|
||||
foreground_active: self.accent_text_color(),
|
||||
foreground_inactive: self.background.on.mix(bg, 0.1),
|
||||
foreground_link: self.link_button.on,
|
||||
foreground_negative: self.destructive_text_color(),
|
||||
foreground_neutral: self.warning_text_color(),
|
||||
foreground_normal: self.background.on,
|
||||
foreground_positive: self.success_text_color(),
|
||||
foreground_visited: self.accent_text_color(),
|
||||
};
|
||||
// components inside the background container
|
||||
let view_colors = IniColors {
|
||||
background_alternate: self.background.component.base.mix(self.accent.base, 0.05),
|
||||
background_normal: self.background.component.base,
|
||||
..window_colors
|
||||
};
|
||||
|
||||
// selected text and items
|
||||
let selection_colors = {
|
||||
// selection colors are swapped to fix menu bar contrast
|
||||
let selected = self.background.component.selected_text;
|
||||
let selected_text = self.background.component.selected;
|
||||
IniColors {
|
||||
background_alternate: selected.mix(bg, 0.5),
|
||||
background_normal: selected,
|
||||
decoration_focus: selected,
|
||||
decoration_hover: selected,
|
||||
foreground_active: selected_text,
|
||||
foreground_inactive: selected_text.mix(selected, 0.5),
|
||||
foreground_link: self.link_button.base,
|
||||
foreground_negative: self.destructive_color(),
|
||||
foreground_neutral: self.warning_color(),
|
||||
foreground_normal: selected_text,
|
||||
foreground_positive: self.success_color(),
|
||||
foreground_visited: self.accent_color(),
|
||||
}
|
||||
};
|
||||
|
||||
let button_colors = IniColors {
|
||||
background_alternate: self.accent_button.base,
|
||||
background_normal: self.button.base,
|
||||
..view_colors
|
||||
};
|
||||
|
||||
// Complementary: Areas of applications with an alternative color scheme; usually with a dark background for light color schemes.
|
||||
let complementary_colors = {
|
||||
let dark = if self.is_dark {
|
||||
self.clone()
|
||||
} else if cfg!(test) {
|
||||
// For reproducible results in tests, use the default dark theme
|
||||
Theme::dark_default()
|
||||
} else {
|
||||
Theme::dark_config()
|
||||
.ok()
|
||||
.as_ref()
|
||||
.and_then(|conf| Theme::get_entry(conf).ok())
|
||||
.unwrap_or_else(|| self.clone())
|
||||
};
|
||||
IniColors {
|
||||
background_alternate: dark.accent.base,
|
||||
background_normal: dark.background.base,
|
||||
decoration_focus: dark.accent_text_color(),
|
||||
decoration_hover: dark.accent_text_color(),
|
||||
foreground_active: dark.accent_text_color(),
|
||||
foreground_inactive: dark.background.on.mix(dark.background.base, 0.1),
|
||||
foreground_link: dark.link_button.on,
|
||||
foreground_negative: dark.destructive_text_color(),
|
||||
foreground_neutral: dark.warning_text_color(),
|
||||
foreground_normal: dark.background.on,
|
||||
foreground_positive: dark.success_text_color(),
|
||||
foreground_visited: dark.accent_text_color(),
|
||||
}
|
||||
};
|
||||
|
||||
// headers in cosmic don't have a background
|
||||
let header_colors = &window_colors;
|
||||
let header_colors_inactive = &window_colors;
|
||||
// tool tips, "What's This" tips, and similar elements
|
||||
let tooltip_colors = &view_colors;
|
||||
|
||||
let general_color_scheme = if self.is_dark {
|
||||
"CosmicDark"
|
||||
} else {
|
||||
"CosmicLight"
|
||||
};
|
||||
let general_name = if self.is_dark {
|
||||
"COSMIC Dark"
|
||||
} else {
|
||||
"COSMIC Light"
|
||||
};
|
||||
// COSMIC icons are stuck in light mode, so use breeze icons instead
|
||||
let icons_theme = if self.is_dark {
|
||||
"breeze-dark"
|
||||
} else {
|
||||
"breeze"
|
||||
};
|
||||
|
||||
format!(
|
||||
r#"# GENERATED BY COSMIC
|
||||
|
||||
[ColorEffects:Disabled]
|
||||
{}
|
||||
|
||||
[ColorEffects:Inactive]
|
||||
ChangeSelectionColor=false
|
||||
Enable=false
|
||||
{}
|
||||
|
||||
[Colors:Button]
|
||||
{}
|
||||
|
||||
[Colors:Complementary]
|
||||
{}
|
||||
|
||||
[Colors:Header]
|
||||
{}
|
||||
|
||||
[Colors:Header][Inactive]
|
||||
{}
|
||||
|
||||
[Colors:Selection]
|
||||
{}
|
||||
|
||||
[Colors:Tooltip]
|
||||
{}
|
||||
|
||||
[Colors:View]
|
||||
{}
|
||||
|
||||
[Colors:Window]
|
||||
{}
|
||||
|
||||
[General]
|
||||
ColorScheme={general_color_scheme}
|
||||
Name={general_name}
|
||||
shadeSortColumn=true
|
||||
|
||||
[Icons]
|
||||
Theme={icons_theme}
|
||||
|
||||
[KDE]
|
||||
contrast=4
|
||||
widgetStyle=qt6ct-style
|
||||
|
||||
[WM]
|
||||
{}
|
||||
"#,
|
||||
format_ini_color_effects(&disabled_color_effects, bg),
|
||||
format_ini_color_effects(&inactive_color_effects, bg),
|
||||
format_ini_colors(&button_colors, bg),
|
||||
format_ini_colors(&complementary_colors, bg),
|
||||
format_ini_colors(&header_colors, bg),
|
||||
format_ini_colors(&header_colors_inactive, bg),
|
||||
format_ini_colors(&selection_colors, bg),
|
||||
format_ini_colors(&tooltip_colors, bg),
|
||||
format_ini_colors(&view_colors, bg),
|
||||
format_ini_colors(&window_colors, bg),
|
||||
format_ini_wm_colors(&window_colors, self.is_dark),
|
||||
)
|
||||
}
|
||||
|
||||
/// Write the color scheme to the appropriate directory.
|
||||
/// Should be written in `~/.local/share/color-schemes/`.
|
||||
///
|
||||
/// See the docs: https://develop.kde.org/docs/plasma/#color-scheme
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `OutputError` if there is an error writing the colors file.
|
||||
#[cold]
|
||||
pub fn write_qt(&self) -> Result<(), OutputError> {
|
||||
let kcolorscheme = self.as_kcolorscheme();
|
||||
let file_path = Self::get_kcolorscheme_path(self.is_dark)?;
|
||||
let tmp_file_path = file_path.with_extension("colors.new");
|
||||
|
||||
// Write to tmp_file_path first, then move it to file_path
|
||||
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
|
||||
let res = tmp_file
|
||||
.write_all(kcolorscheme.as_bytes())
|
||||
.and_then(|_| tmp_file.flush())
|
||||
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
|
||||
if let Err(e) = res {
|
||||
_ = std::fs::remove_file(&tmp_file_path);
|
||||
return Err(OutputError::Io(e));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply the color scheme by copying its values to `~/.config/kdeglobals`.
|
||||
///
|
||||
/// See the docs: https://develop.kde.org/docs/plasma/#color-scheme
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `OutputError` if there is an error applying the color scheme.
|
||||
#[cold]
|
||||
pub fn apply_qt(is_dark: bool) -> Result<(), OutputError> {
|
||||
let Some(config_dir) = dirs::config_dir() else {
|
||||
return Err(OutputError::MissingConfigDir);
|
||||
};
|
||||
let kdeglobals_file = config_dir.join("kdeglobals");
|
||||
let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?;
|
||||
|
||||
let src_file = Self::get_kcolorscheme_path(is_dark)?;
|
||||
let src_ini = Self::read_ini(&src_file)?;
|
||||
|
||||
Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file)
|
||||
.map_err(OutputError::Io)?;
|
||||
|
||||
for (section, key_value) in src_ini.get_map_ref() {
|
||||
for (key, value) in key_value {
|
||||
kdeglobals_ini.set(section, key, value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
kdeglobals_ini
|
||||
.pretty_write(kdeglobals_file, &qt_settings_ini_style())
|
||||
.map_err(OutputError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset the applied qt colors by removing color scheme values from the
|
||||
/// `~/.config/kdeglobals` file.
|
||||
///
|
||||
/// This does not restore the backed up kdeglobals file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `OutputError` if there is an error resetting the CSS file.
|
||||
#[cold]
|
||||
pub fn reset_qt() -> Result<(), OutputError> {
|
||||
let Some(config_dir) = dirs::config_dir() else {
|
||||
return Err(OutputError::MissingConfigDir);
|
||||
};
|
||||
let kdeglobals_file = config_dir.join("kdeglobals");
|
||||
let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?;
|
||||
|
||||
if !Self::is_cosmic_kdeglobals(&kdeglobals_ini)
|
||||
.map_err(OutputError::Io)?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Not a cosmic kdeglobals file, do nothing
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let is_dark = false; // doesn't matter since we're only reading keys
|
||||
let src_file = Self::get_kcolorscheme_path(is_dark)?;
|
||||
let src_ini = Self::read_ini(&src_file)?;
|
||||
|
||||
for (section, key_value) in src_ini.get_map_ref() {
|
||||
for (key, _) in key_value {
|
||||
kdeglobals_ini.remove_key(section, key);
|
||||
}
|
||||
}
|
||||
|
||||
kdeglobals_ini
|
||||
.write(kdeglobals_file)
|
||||
.map_err(OutputError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors`
|
||||
fn get_kcolorscheme_path(is_dark: bool) -> Result<PathBuf, OutputError> {
|
||||
let Some(mut data_dir) = dirs::data_dir() else {
|
||||
return Err(OutputError::MissingDataDir);
|
||||
};
|
||||
|
||||
let file_name = if is_dark {
|
||||
"CosmicDark.colors"
|
||||
} else {
|
||||
"CosmicLight.colors"
|
||||
};
|
||||
|
||||
data_dir.push("color-schemes");
|
||||
if !data_dir.exists() {
|
||||
std::fs::create_dir_all(&data_dir).map_err(OutputError::Io)?;
|
||||
}
|
||||
|
||||
Ok(data_dir.join(file_name))
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn read_ini(path: &PathBuf) -> Result<Ini, OutputError> {
|
||||
let mut ini = Ini::new_cs();
|
||||
if !path.exists() {
|
||||
return Ok(ini);
|
||||
}
|
||||
let file_content = fs::read_to_string(path).map_err(OutputError::Io)?;
|
||||
ini.read(file_content).map_err(OutputError::Ini)?;
|
||||
Ok(ini)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn backup_non_cosmic_kdeglobals(ini: &Ini, path: &Path) -> io::Result<()> {
|
||||
if !Self::is_cosmic_kdeglobals(&ini)?.unwrap_or(true) {
|
||||
let backup_path = path.with_extension("bak");
|
||||
fs::copy(path, &backup_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn is_cosmic_kdeglobals(ini: &Ini) -> io::Result<Option<bool>> {
|
||||
let color_scheme = ini.get("General", "ColorScheme");
|
||||
if let Some(color_scheme) = color_scheme {
|
||||
Ok(Some(
|
||||
color_scheme == "CosmicDark" || color_scheme == "CosmicLight",
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a color in the form `r,g,b` e.g. `255,255,255`.
|
||||
/// If the color has transparency, it is mixed with bg first.
|
||||
fn to_rgb(c: Srgba, bg: Srgba) -> String {
|
||||
let c_u8: Srgba<u8> = c.over(bg).into_format();
|
||||
format!("{},{},{}", c_u8.red, c_u8.green, c_u8.blue)
|
||||
}
|
||||
|
||||
fn format_ini_color_effects(color_effects: &IniColorEffects, bg: Srgba) -> String {
|
||||
format!(
|
||||
r#"Color={}
|
||||
ColorAmount={}
|
||||
ColorEffect={}
|
||||
ContrastAmount={}
|
||||
ContrastEffect={}
|
||||
IntensityAmount={}
|
||||
IntensityEffect={}"#,
|
||||
to_rgb(color_effects.color, bg),
|
||||
color_effects.color_amount,
|
||||
color_effects.color_effect.as_u8(),
|
||||
color_effects.contrast_amount,
|
||||
color_effects.contrast_effect.as_u8(),
|
||||
color_effects.intensity_amount,
|
||||
color_effects.intensity_effect.as_u8(),
|
||||
)
|
||||
}
|
||||
|
||||
fn format_ini_colors(colors: &IniColors, bg: Srgba) -> String {
|
||||
format!(
|
||||
r#"BackgroundAlternate={}
|
||||
BackgroundNormal={}
|
||||
DecorationFocus={}
|
||||
DecorationHover={}
|
||||
ForegroundActive={}
|
||||
ForegroundInactive={}
|
||||
ForegroundLink={}
|
||||
ForegroundNegative={}
|
||||
ForegroundNeutral={}
|
||||
ForegroundNormal={}
|
||||
ForegroundPositive={}
|
||||
ForegroundVisited={}"#,
|
||||
to_rgb(colors.background_alternate, bg),
|
||||
to_rgb(colors.background_normal, bg),
|
||||
to_rgb(colors.decoration_focus, bg),
|
||||
to_rgb(colors.decoration_hover, bg),
|
||||
to_rgb(colors.foreground_active, bg),
|
||||
to_rgb(colors.foreground_inactive, bg),
|
||||
to_rgb(colors.foreground_link, bg),
|
||||
to_rgb(colors.foreground_negative, bg),
|
||||
to_rgb(colors.foreground_neutral, bg),
|
||||
to_rgb(colors.foreground_normal, bg),
|
||||
to_rgb(colors.foreground_positive, bg),
|
||||
to_rgb(colors.foreground_visited, bg),
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the colors for the titlebars of active and inactive windows.
|
||||
fn format_ini_wm_colors(view_colors: &IniColors, is_dark: bool) -> String {
|
||||
let bg = view_colors.background_normal;
|
||||
let fg = view_colors.foreground_active;
|
||||
let blend = if is_dark { fg } else { bg };
|
||||
|
||||
format!(
|
||||
r#"activeBackground={}
|
||||
activeBlend={}
|
||||
activeForeground={}
|
||||
inactiveBackground={}
|
||||
inactiveBlend={}
|
||||
inactiveForeground={}"#,
|
||||
to_rgb(bg, bg),
|
||||
to_rgb(blend, bg),
|
||||
to_rgb(fg, bg),
|
||||
to_rgb(bg, bg),
|
||||
to_rgb(blend, bg),
|
||||
to_rgb(fg, bg),
|
||||
)
|
||||
}
|
||||
|
||||
struct IniColorEffects {
|
||||
color: Srgba,
|
||||
color_amount: f32,
|
||||
color_effect: ColorEffect,
|
||||
contrast_amount: f32,
|
||||
/// Applied to the text, using the background as the reference color.
|
||||
contrast_effect: ColorEffect,
|
||||
intensity_amount: f32,
|
||||
intensity_effect: IntensityEffect,
|
||||
}
|
||||
/// Each color set is made up of a number of roles which are available in all other sets.
|
||||
/// In addition, except for Inactive Text, there is a corresponding background role for each of the text roles. Currently (except for Normal and Alternate Background), these colors are not chosen here but are automatically determined based on Normal Background and the corresponding Text color.
|
||||
struct IniColors {
|
||||
/// used when there is a need to subtly change the background to aid in item association. This might be used e.g. as the background of a heading, but is mostly used for alternating rows in lists, especially multi-column lists, to aid in visually tracking rows.
|
||||
background_alternate: Srgba,
|
||||
/// Normal background
|
||||
background_normal: Srgba,
|
||||
/// Used for drawing lines or shading UI elements to indicate the item which has active input focus.
|
||||
/// Typically the same as foreground_active.
|
||||
decoration_focus: Srgba,
|
||||
/// Used for drawing lines or shading UI elements for mouse-over effects, e.g. the "illumination" effects for buttons.
|
||||
/// Typically the same as foreground_active.
|
||||
decoration_hover: Srgba,
|
||||
/// used to indicate an active element or attract attention, e.g. alerts, notifications; also for hovered hyperlinks
|
||||
foreground_active: Srgba,
|
||||
/// used for text which should be unobtrusive, e.g. comments, "subtitles", unimportant information, etc.
|
||||
foreground_inactive: Srgba,
|
||||
/// used for hyperlinks or to otherwise indicate "something which may be visited", or to show relationships
|
||||
foreground_link: Srgba,
|
||||
/// used for errors, failure notices, notifications that an action may be dangerous (e.g. unsafe web page or security context), etc.
|
||||
foreground_negative: Srgba,
|
||||
/// used to draw attention when another role is not appropriate; e.g. warnings, to indicate secure/encrypted content, etc.
|
||||
foreground_neutral: Srgba,
|
||||
/// Normal foreground
|
||||
foreground_normal: Srgba,
|
||||
/// used for success notices, to indicate trusted content, etc.
|
||||
foreground_positive: Srgba,
|
||||
/// used for "something (e.g. a hyperlink) that has been visited", or to indicate something that is "old".
|
||||
foreground_visited: Srgba,
|
||||
}
|
||||
|
||||
/// Intensity allows the overall color to be lightened or darkened.
|
||||
#[allow(dead_code)]
|
||||
enum IntensityEffect {
|
||||
/// Makes everything lighter or darker in a controlled manner.
|
||||
///
|
||||
/// intensity_amount increases or decreases the overall intensity (i.e. perceived brightness) by an absolute amount.
|
||||
Shade,
|
||||
/// Changes the intensity to a percentage of the initial value.
|
||||
Darken,
|
||||
/// Conceptually the opposite of darken; lighten can be thought of as working with "distance from white", where darken works with "distance from black".
|
||||
Lighten,
|
||||
}
|
||||
|
||||
impl IntensityEffect {
|
||||
pub fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
Self::Shade => 0,
|
||||
Self::Darken => 1,
|
||||
Self::Lighten => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This also changes the overall color like [IntensityEffect],
|
||||
/// but is not limited to intensity.
|
||||
#[allow(dead_code)]
|
||||
enum ColorEffect {
|
||||
/// changes the relative chroma
|
||||
///
|
||||
/// This is available for "ColorEffect" but not "ContrastEffect".
|
||||
Desaturate,
|
||||
/// smoothly blends the original color into a reference color
|
||||
Fade,
|
||||
/// similar to Fade, except that the color (hue and chroma) changes more quickly while the intensity changes more slowly as the amount is increased
|
||||
Tint,
|
||||
}
|
||||
|
||||
impl ColorEffect {
|
||||
pub fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
Self::Desaturate => 0,
|
||||
Self::Fade => 1,
|
||||
Self::Tint => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_opaque_color_to_rgb() {
|
||||
let color = Srgba::new(30.0 / 255.0, 50.0 / 255.0, 70.0 / 255.0, 1.0);
|
||||
let bg = Srgba::new(1.0, 1.0, 1.0, 1.0);
|
||||
let result = to_rgb(color, bg);
|
||||
assert_eq!(result, "30,50,70");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparent_color_to_rgb() {
|
||||
let color = Srgba::new(0.0, 0.0, 0.0, 0.0);
|
||||
let bg = Srgba::new(1.0, 1.0, 1.0, 1.0);
|
||||
let result = to_rgb(color, bg);
|
||||
assert_eq!(result, "255,255,255");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translucent_color_to_rgb() {
|
||||
let color = Srgba::new(0.0, 0.0, 0.0, 0.9);
|
||||
let bg = Srgba::new(1.0, 1.0, 1.0, 1.0);
|
||||
let result = to_rgb(color, bg);
|
||||
assert_eq!(result, "26,26,26");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_light_default_kcolorscheme() {
|
||||
let light_default_kcolorscheme = Theme::light_default().as_kcolorscheme();
|
||||
insta::assert_snapshot!(light_default_kcolorscheme);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dark_default_kcolorscheme() {
|
||||
let dark_default_kcolorscheme = Theme::dark_default().as_kcolorscheme();
|
||||
insta::assert_snapshot!(dark_default_kcolorscheme);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
source: cosmic-theme/src/output/qt56ct_output.rs
|
||||
expression: dark_default_qpalette
|
||||
---
|
||||
# GENERATED BY COSMIC
|
||||
|
||||
[ColorScheme]
|
||||
active_colors=#ffe7e7e7, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffc0c0c0, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff434343, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffc0c0c0, #ff777777
|
||||
disabled_colors=#e6d3d3d3, #8f474747, #a9696969, #a4626262, #a95f5f5f, #a45d5d5d, #d2a1a1a1, #ffe7e7e7, #d2a1a1a1, #bf2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #bf3c3c3c, #bf30555a, #bf324f53, #ff1f2425, #bf2e2e2e, #bf2e2e2e, #d2a1a1a1, #bf909090
|
||||
inactive_colors=#ffc2c2c2, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffa3a3a3, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff3f3f3f, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffa3a3a3, #ff777777
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
source: cosmic-theme/src/output/qt56ct_output.rs
|
||||
expression: light_default_qpalette
|
||||
---
|
||||
# GENERATED BY COSMIC
|
||||
|
||||
[ColorScheme]
|
||||
active_colors=#ff121212, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff272727, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff272727, #ff8e8e8e
|
||||
disabled_colors=#e62b2b2b, #8fc9c9c9, #a99b9b9b, #a4a0a0a0, #a9929292, #a49b9b9b, #d2535353, #ffd7d7d7, #d2535353, #bff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #bff6f6f6, #bf526d70, #bf72888a, #ffccd0d1, #bff5f5f5, #bff5f5f5, #d2535353, #bf6c6c6c
|
||||
inactive_colors=#ff3f3f3f, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff505050, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff505050, #ff8e8e8e
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
---
|
||||
source: cosmic-theme/src/output/qt_output.rs
|
||||
expression: dark_default_kcolorscheme
|
||||
---
|
||||
# GENERATED BY COSMIC
|
||||
|
||||
[ColorEffects:Disabled]
|
||||
Color=43,43,43
|
||||
ColorAmount=0
|
||||
ColorEffect=0
|
||||
ContrastAmount=0.65
|
||||
ContrastEffect=1
|
||||
IntensityAmount=0.1
|
||||
IntensityEffect=2
|
||||
|
||||
[ColorEffects:Inactive]
|
||||
ChangeSelectionColor=false
|
||||
Enable=false
|
||||
Color=27,27,27
|
||||
ColorAmount=0.025
|
||||
ColorEffect=2
|
||||
ContrastAmount=0.1
|
||||
ContrastEffect=2
|
||||
IntensityAmount=0
|
||||
IntensityEffect=0
|
||||
|
||||
[Colors:Button]
|
||||
BackgroundAlternate=99,208,223
|
||||
BackgroundNormal=60,60,60
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=99,208,223
|
||||
ForegroundInactive=211,211,211
|
||||
ForegroundLink=99,208,223
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=231,231,231
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[Colors:Complementary]
|
||||
BackgroundAlternate=99,208,223
|
||||
BackgroundNormal=27,27,27
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=99,208,223
|
||||
ForegroundInactive=211,211,211
|
||||
ForegroundLink=99,208,223
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=231,231,231
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[Colors:Header]
|
||||
BackgroundAlternate=31,36,37
|
||||
BackgroundNormal=27,27,27
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=99,208,223
|
||||
ForegroundInactive=211,211,211
|
||||
ForegroundLink=99,208,223
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=231,231,231
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[Colors:Header][Inactive]
|
||||
BackgroundAlternate=31,36,37
|
||||
BackgroundNormal=27,27,27
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=99,208,223
|
||||
ForegroundInactive=211,211,211
|
||||
ForegroundLink=99,208,223
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=231,231,231
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[Colors:Selection]
|
||||
BackgroundAlternate=63,118,125
|
||||
BackgroundNormal=99,208,223
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=67,67,67
|
||||
ForegroundInactive=83,138,145
|
||||
ForegroundLink=27,27,27
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=67,67,67
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[Colors:Tooltip]
|
||||
BackgroundAlternate=49,55,55
|
||||
BackgroundNormal=46,46,46
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=99,208,223
|
||||
ForegroundInactive=211,211,211
|
||||
ForegroundLink=99,208,223
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=231,231,231
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[Colors:View]
|
||||
BackgroundAlternate=49,55,55
|
||||
BackgroundNormal=46,46,46
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=99,208,223
|
||||
ForegroundInactive=211,211,211
|
||||
ForegroundLink=99,208,223
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=231,231,231
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[Colors:Window]
|
||||
BackgroundAlternate=31,36,37
|
||||
BackgroundNormal=27,27,27
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=99,208,223
|
||||
ForegroundInactive=211,211,211
|
||||
ForegroundLink=99,208,223
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=231,231,231
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[General]
|
||||
ColorScheme=CosmicDark
|
||||
Name=COSMIC Dark
|
||||
shadeSortColumn=true
|
||||
|
||||
[Icons]
|
||||
Theme=breeze-dark
|
||||
|
||||
[KDE]
|
||||
contrast=4
|
||||
widgetStyle=qt6ct-style
|
||||
|
||||
[WM]
|
||||
activeBackground=27,27,27
|
||||
activeBlend=99,208,223
|
||||
activeForeground=99,208,223
|
||||
inactiveBackground=27,27,27
|
||||
inactiveBlend=99,208,223
|
||||
inactiveForeground=99,208,223
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
---
|
||||
source: cosmic-theme/src/output/qt_output.rs
|
||||
expression: light_default_kcolorscheme
|
||||
---
|
||||
# GENERATED BY COSMIC
|
||||
|
||||
[ColorEffects:Disabled]
|
||||
Color=194,194,194
|
||||
ColorAmount=0
|
||||
ColorEffect=0
|
||||
ContrastAmount=0.65
|
||||
ContrastEffect=1
|
||||
IntensityAmount=0.1
|
||||
IntensityEffect=2
|
||||
|
||||
[ColorEffects:Inactive]
|
||||
ChangeSelectionColor=false
|
||||
Enable=false
|
||||
Color=215,215,215
|
||||
ColorAmount=0.025
|
||||
ColorEffect=2
|
||||
ContrastAmount=0.1
|
||||
ContrastEffect=2
|
||||
IntensityAmount=0
|
||||
IntensityEffect=0
|
||||
|
||||
[Colors:Button]
|
||||
BackgroundAlternate=0,82,90
|
||||
BackgroundNormal=173,173,173
|
||||
DecorationFocus=0,82,90
|
||||
DecorationHover=0,82,90
|
||||
ForegroundActive=0,82,90
|
||||
ForegroundInactive=38,38,38
|
||||
ForegroundLink=0,82,90
|
||||
ForegroundNegative=137,4,24
|
||||
ForegroundNeutral=121,44,0
|
||||
ForegroundNormal=18,18,18
|
||||
ForegroundPositive=0,87,44
|
||||
ForegroundVisited=0,82,90
|
||||
|
||||
[Colors:Complementary]
|
||||
BackgroundAlternate=99,208,223
|
||||
BackgroundNormal=27,27,27
|
||||
DecorationFocus=99,208,223
|
||||
DecorationHover=99,208,223
|
||||
ForegroundActive=99,208,223
|
||||
ForegroundInactive=211,211,211
|
||||
ForegroundLink=99,208,223
|
||||
ForegroundNegative=255,160,154
|
||||
ForegroundNeutral=255,163,125
|
||||
ForegroundNormal=231,231,231
|
||||
ForegroundPositive=94,219,140
|
||||
ForegroundVisited=99,208,223
|
||||
|
||||
[Colors:Header]
|
||||
BackgroundAlternate=204,208,209
|
||||
BackgroundNormal=215,215,215
|
||||
DecorationFocus=0,82,90
|
||||
DecorationHover=0,82,90
|
||||
ForegroundActive=0,82,90
|
||||
ForegroundInactive=38,38,38
|
||||
ForegroundLink=0,82,90
|
||||
ForegroundNegative=137,4,24
|
||||
ForegroundNeutral=121,44,0
|
||||
ForegroundNormal=18,18,18
|
||||
ForegroundPositive=0,87,44
|
||||
ForegroundVisited=0,82,90
|
||||
|
||||
[Colors:Header][Inactive]
|
||||
BackgroundAlternate=204,208,209
|
||||
BackgroundNormal=215,215,215
|
||||
DecorationFocus=0,82,90
|
||||
DecorationHover=0,82,90
|
||||
ForegroundActive=0,82,90
|
||||
ForegroundInactive=38,38,38
|
||||
ForegroundLink=0,82,90
|
||||
ForegroundNegative=137,4,24
|
||||
ForegroundNeutral=121,44,0
|
||||
ForegroundNormal=18,18,18
|
||||
ForegroundPositive=0,87,44
|
||||
ForegroundVisited=0,82,90
|
||||
|
||||
[Colors:Selection]
|
||||
BackgroundAlternate=108,149,152
|
||||
BackgroundNormal=0,82,90
|
||||
DecorationFocus=0,82,90
|
||||
DecorationHover=0,82,90
|
||||
ForegroundActive=246,246,246
|
||||
ForegroundInactive=123,164,168
|
||||
ForegroundLink=215,215,215
|
||||
ForegroundNegative=137,4,24
|
||||
ForegroundNeutral=121,44,0
|
||||
ForegroundNormal=246,246,246
|
||||
ForegroundPositive=0,87,44
|
||||
ForegroundVisited=0,82,90
|
||||
|
||||
[Colors:Tooltip]
|
||||
BackgroundAlternate=233,237,237
|
||||
BackgroundNormal=245,245,245
|
||||
DecorationFocus=0,82,90
|
||||
DecorationHover=0,82,90
|
||||
ForegroundActive=0,82,90
|
||||
ForegroundInactive=38,38,38
|
||||
ForegroundLink=0,82,90
|
||||
ForegroundNegative=137,4,24
|
||||
ForegroundNeutral=121,44,0
|
||||
ForegroundNormal=18,18,18
|
||||
ForegroundPositive=0,87,44
|
||||
ForegroundVisited=0,82,90
|
||||
|
||||
[Colors:View]
|
||||
BackgroundAlternate=233,237,237
|
||||
BackgroundNormal=245,245,245
|
||||
DecorationFocus=0,82,90
|
||||
DecorationHover=0,82,90
|
||||
ForegroundActive=0,82,90
|
||||
ForegroundInactive=38,38,38
|
||||
ForegroundLink=0,82,90
|
||||
ForegroundNegative=137,4,24
|
||||
ForegroundNeutral=121,44,0
|
||||
ForegroundNormal=18,18,18
|
||||
ForegroundPositive=0,87,44
|
||||
ForegroundVisited=0,82,90
|
||||
|
||||
[Colors:Window]
|
||||
BackgroundAlternate=204,208,209
|
||||
BackgroundNormal=215,215,215
|
||||
DecorationFocus=0,82,90
|
||||
DecorationHover=0,82,90
|
||||
ForegroundActive=0,82,90
|
||||
ForegroundInactive=38,38,38
|
||||
ForegroundLink=0,82,90
|
||||
ForegroundNegative=137,4,24
|
||||
ForegroundNeutral=121,44,0
|
||||
ForegroundNormal=18,18,18
|
||||
ForegroundPositive=0,87,44
|
||||
ForegroundVisited=0,82,90
|
||||
|
||||
[General]
|
||||
ColorScheme=CosmicLight
|
||||
Name=COSMIC Light
|
||||
shadeSortColumn=true
|
||||
|
||||
[Icons]
|
||||
Theme=breeze
|
||||
|
||||
[KDE]
|
||||
contrast=4
|
||||
widgetStyle=qt6ct-style
|
||||
|
||||
[WM]
|
||||
activeBackground=215,215,215
|
||||
activeBlend=215,215,215
|
||||
activeForeground=0,82,90
|
||||
inactiveBackground=215,215,215
|
||||
inactiveBlend=215,215,215
|
||||
inactiveForeground=0,82,90
|
||||
|
|
@ -145,6 +145,7 @@ pub fn is_valid_srgb(c: Srgba) -> bool {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use almost::equal;
|
||||
use palette::{OklabHue, Srgba};
|
||||
|
||||
use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma};
|
||||
|
|
@ -172,57 +173,57 @@ mod tests {
|
|||
fn test_conversion_boundaries() {
|
||||
let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
|
||||
let srgb = oklch_to_srgba_nearest_chroma(c1);
|
||||
almost::zero(srgb.red);
|
||||
almost::zero(srgb.blue);
|
||||
almost::zero(srgb.green);
|
||||
equal(srgb.red, 0.0);
|
||||
equal(srgb.blue, 0.0);
|
||||
equal(srgb.green, 0.0);
|
||||
|
||||
let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
|
||||
let srgb = oklch_to_srgba_nearest_chroma(c1);
|
||||
|
||||
almost::equal(srgb.red, 1.0);
|
||||
almost::equal(srgb.blue, 1.0);
|
||||
almost::equal(srgb.green, 1.0);
|
||||
equal(srgb.red, 1.0);
|
||||
equal(srgb.blue, 1.0);
|
||||
equal(srgb.green, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion_colors() {
|
||||
let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0);
|
||||
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
|
||||
assert_eq!(srgb.red, 133);
|
||||
assert_eq!(srgb.green, 69);
|
||||
assert_eq!(srgb.blue, 0);
|
||||
assert!(srgb.red == 133);
|
||||
assert!(srgb.green == 69);
|
||||
assert!(srgb.blue == 0);
|
||||
|
||||
let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0);
|
||||
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
|
||||
assert_eq!(srgb.red, 78);
|
||||
assert_eq!(srgb.green, 27);
|
||||
assert_eq!(srgb.blue, 15);
|
||||
assert!(srgb.red == 78);
|
||||
assert!(srgb.green == 27);
|
||||
assert!(srgb.blue == 15);
|
||||
|
||||
let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0);
|
||||
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
|
||||
assert_eq!(srgb.red, 192);
|
||||
assert_eq!(srgb.green, 153);
|
||||
assert_eq!(srgb.blue, 253);
|
||||
assert!(srgb.red == 192);
|
||||
assert!(srgb.green == 153);
|
||||
assert!(srgb.blue == 253);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion_fallback_colors() {
|
||||
let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0);
|
||||
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
|
||||
assert_eq!(srgb.red, 255);
|
||||
assert_eq!(srgb.green, 102);
|
||||
assert_eq!(srgb.blue, 65);
|
||||
assert!(srgb.red == 255);
|
||||
assert!(srgb.green == 103);
|
||||
assert!(srgb.blue == 65);
|
||||
|
||||
let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0);
|
||||
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
|
||||
assert_eq!(srgb.red, 193);
|
||||
assert_eq!(srgb.green, 152);
|
||||
assert_eq!(srgb.blue, 255);
|
||||
assert!(srgb.red == 193);
|
||||
assert!(srgb.green == 152);
|
||||
assert!(srgb.blue == 255);
|
||||
|
||||
let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0);
|
||||
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
|
||||
assert_eq!(srgb.red, 1);
|
||||
assert_eq!(srgb.green, 19);
|
||||
assert_eq!(srgb.blue, 0);
|
||||
assert!(srgb.red == 1);
|
||||
assert!(srgb.green == 19);
|
||||
assert!(srgb.blue == 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
open = "5.3.3"
|
||||
open = "5.3.2"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
path = "../../"
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ impl cosmic::Application for App {
|
|||
fn view(&self) -> Element<'_, Self::Message> {
|
||||
let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout);
|
||||
let centered = cosmic::widget::container(
|
||||
widget::column::with_capacity(1)
|
||||
widget::column()
|
||||
.push(show_about_button)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
once_cell = "1"
|
||||
rust-embed = "8.11.0"
|
||||
rust-embed = "8.6.0"
|
||||
tracing = "0.1"
|
||||
env_logger = "0.10.2"
|
||||
log = "0.4.29"
|
||||
log = "0.4.26"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
path = "../../"
|
||||
git = "https://github.com/pop-os/libcosmic"
|
||||
default-features = false
|
||||
features = ["applet-token"]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use cosmic::app::{Core, Task};
|
||||
|
||||
use cosmic::iced::core::window;
|
||||
use cosmic::iced::window::Id;
|
||||
use cosmic::iced::{Length, Rectangle};
|
||||
use cosmic::iced_runtime::core::window;
|
||||
use cosmic::surface::action::{app_popup, destroy_popup};
|
||||
use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler};
|
||||
use cosmic::Element;
|
||||
|
|
@ -13,7 +13,6 @@ pub struct Window {
|
|||
core: Core,
|
||||
popup: Option<Id>,
|
||||
example_row: bool,
|
||||
toggle: bool,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +22,6 @@ impl Default for Window {
|
|||
core: Core::default(),
|
||||
popup: None,
|
||||
example_row: false,
|
||||
toggle: false,
|
||||
selected: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +33,6 @@ pub enum Message {
|
|||
ToggleExampleRow(bool),
|
||||
Selected(usize),
|
||||
Surface(cosmic::surface::Action),
|
||||
Toggle(bool),
|
||||
}
|
||||
|
||||
impl cosmic::Application for Window {
|
||||
|
|
@ -74,6 +71,7 @@ impl cosmic::Application for Window {
|
|||
Message::ToggleExampleRow(toggled) => {
|
||||
self.example_row = toggled;
|
||||
}
|
||||
|
||||
Message::Surface(a) => {
|
||||
return cosmic::task::message(cosmic::Action::Cosmic(
|
||||
cosmic::app::Action::Surface(a),
|
||||
|
|
@ -82,9 +80,6 @@ impl cosmic::Application for Window {
|
|||
Message::Selected(i) => {
|
||||
self.selected = Some(i);
|
||||
}
|
||||
Message::Toggle(v) => {
|
||||
self.toggle = v;
|
||||
}
|
||||
};
|
||||
Task::none()
|
||||
}
|
||||
|
|
@ -128,8 +123,9 @@ impl cosmic::Application for Window {
|
|||
"Example row",
|
||||
cosmic::widget::container(
|
||||
toggler(state.example_row)
|
||||
.on_toggle(Message::ToggleExampleRow),
|
||||
),
|
||||
.on_toggle(|value| Message::ToggleExampleRow(value)),
|
||||
)
|
||||
.height(Length::Fixed(50.)),
|
||||
))
|
||||
.add(popup_dropdown(
|
||||
&["1", "asdf", "hello", "test"],
|
||||
|
|
@ -159,7 +155,7 @@ impl cosmic::Application for Window {
|
|||
"oops".into()
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<cosmic::iced::theme::Style> {
|
||||
fn style(&self) -> Option<cosmic::iced_runtime::Appearance> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ default = ["wayland"]
|
|||
wayland = ["libcosmic/wayland"]
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.11"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-log = "0.2.0"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
path = "../../"
|
||||
|
|
@ -18,8 +20,7 @@ features = [
|
|||
"tokio",
|
||||
"xdg-portal",
|
||||
"a11y",
|
||||
"wgpu",
|
||||
"single-instance",
|
||||
"surface-message",
|
||||
"multi-window",
|
||||
"wgpu",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -54,9 +54,8 @@ impl widget::menu::Action for Action {
|
|||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
|
||||
// tracing_subscriber::fmt::init();
|
||||
// let _ = tracing_log::LogTracer::init();
|
||||
|
||||
let input = vec![
|
||||
(Page::Page1, "🖖 Hello from libcosmic.".into()),
|
||||
|
|
@ -67,7 +66,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
let settings = Settings::default()
|
||||
.size(Size::new(1024., 768.));
|
||||
cosmic::app::run::<App>(settings, input).unwrap();
|
||||
|
||||
cosmic::app::run::<App>(settings, input)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +83,6 @@ pub enum Message {
|
|||
Hi,
|
||||
Hi2,
|
||||
Hi3,
|
||||
Tick,
|
||||
}
|
||||
|
||||
/// The [`App`] stores application-specific state.
|
||||
|
|
@ -93,7 +93,6 @@ pub struct App {
|
|||
input_2: String,
|
||||
hidden: bool,
|
||||
keybinds: HashMap<KeyBind, Action>,
|
||||
progress: f32,
|
||||
}
|
||||
|
||||
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||
|
|
@ -135,7 +134,6 @@ impl cosmic::Application for App {
|
|||
input_2: String::new(),
|
||||
hidden: true,
|
||||
keybinds: HashMap::new(),
|
||||
progress: 0.0,
|
||||
};
|
||||
|
||||
let command = app.update_title();
|
||||
|
|
@ -181,17 +179,10 @@ impl cosmic::Application for App {
|
|||
Message::Hi3 => {
|
||||
dbg!("hi 3");
|
||||
}
|
||||
Message::Tick => {
|
||||
self.progress = (self.progress + 0.01) % 1.0;
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> iced::Subscription<Self::Message> {
|
||||
iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick)
|
||||
}
|
||||
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<'_, Self::Message> {
|
||||
let page_content = self
|
||||
|
|
@ -200,7 +191,7 @@ impl cosmic::Application for App {
|
|||
.map_or("No page selected", String::as_str);
|
||||
|
||||
let centered = widget::container(
|
||||
widget::column::with_capacity(14)
|
||||
widget::column()
|
||||
.push(widget::text::body(page_content))
|
||||
.push(
|
||||
widget::text_input::text_input("", &self.input_1)
|
||||
|
|
@ -222,47 +213,6 @@ impl cosmic::Application for App {
|
|||
.on_input(Message::Input2)
|
||||
.on_clear(Message::Ignore),
|
||||
)
|
||||
.push(widget::progress_bar::circular::Circular::new().size(50.0))
|
||||
.push(widget::progress_bar::circular::Circular::new().size(20.0))
|
||||
.push(
|
||||
widget::progress_bar::linear::Linear::new()
|
||||
.girth(10.0)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
widget::progress_bar::circular::Circular::new()
|
||||
.bar_height(10.0)
|
||||
.size(50.0)
|
||||
.progress(self.progress),
|
||||
)
|
||||
.push(
|
||||
widget::progress_bar::linear::Linear::new()
|
||||
.girth(10.0)
|
||||
.progress(self.progress)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
widget::progress_bar::circular::Circular::new()
|
||||
.size(50.0)
|
||||
.progress(0.0),
|
||||
)
|
||||
.push(
|
||||
widget::progress_bar::linear::Linear::new()
|
||||
.girth(10.0)
|
||||
.progress(0.0)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
widget::progress_bar::circular::Circular::new()
|
||||
.size(50.0)
|
||||
.progress(1.0),
|
||||
)
|
||||
.push(
|
||||
widget::progress_bar::linear::Linear::new()
|
||||
.girth(10.0)
|
||||
.progress(1.0)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.spacing(cosmic::theme::spacing().space_s)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
[package]
|
||||
name = "calendar"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
jiff = "0.2"
|
||||
chrono = "0.4.40"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
path = "../../"
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
|
||||
//! Calendar widget example
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::widget::calendar::CalendarModel;
|
||||
use cosmic::{ApplicationExt, Element, executor, iced};
|
||||
use jiff::civil::{Date, Weekday};
|
||||
use cosmic::{executor, iced, ApplicationExt, Element};
|
||||
|
||||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
|
|
@ -19,7 +19,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
/// Messages that are used specifically by our [`App`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
DateSelected(Date),
|
||||
DateSelected(NaiveDate),
|
||||
PrevMonth,
|
||||
NextMonth,
|
||||
}
|
||||
|
|
@ -85,15 +85,19 @@ impl cosmic::Application for App {
|
|||
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<'_, Self::Message> {
|
||||
let mut content = cosmic::widget::column().spacing(12);
|
||||
|
||||
let calendar = cosmic::widget::calendar(
|
||||
&self.calendar_model,
|
||||
|date| Message::DateSelected(date),
|
||||
|| Message::PrevMonth,
|
||||
|| Message::NextMonth,
|
||||
Weekday::Sunday,
|
||||
chrono::Weekday::Sun,
|
||||
);
|
||||
|
||||
let centered = cosmic::widget::container(calendar)
|
||||
content = content.push(calendar);
|
||||
|
||||
let centered = cosmic::widget::container(content)
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Shrink)
|
||||
.align_x(iced::Alignment::Center)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-log = "0.2.0"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! Application API example
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::Size;
|
||||
use cosmic::iced_core::Size;
|
||||
use cosmic::widget::menu;
|
||||
use cosmic::{executor, iced, ApplicationExt, Element};
|
||||
use std::collections::HashMap;
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ libcosmic = { path = "../..", features = [
|
|||
"xdg-portal",
|
||||
] }
|
||||
once_cell = "1.21"
|
||||
slotmap = "1.1.1"
|
||||
slotmap = "1.0.7"
|
||||
env_logger = "0.10"
|
||||
log = "0.4.29"
|
||||
log = "0.4.26"
|
||||
|
||||
[dependencies.cosmic-time]
|
||||
git = "https://github.com/pop-os/cosmic-time"
|
||||
|
|
|
|||
|
|
@ -28,14 +28,13 @@ impl State {
|
|||
column!(
|
||||
list_column().add(settings::item(
|
||||
"Bluetooth",
|
||||
toggler(self.enabled).on_toggle(Message::Enable)
|
||||
toggler(None, self.enabled, Message::Enable)
|
||||
)),
|
||||
text("Now visible as \"TODO\", just kidding")
|
||||
)
|
||||
.spacing(8)
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Devices")
|
||||
settings::view_section("Devices")
|
||||
.add(settings::item("No devices found", text("")))
|
||||
.into(),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -258,13 +258,12 @@ impl State {
|
|||
match self.tab_bar.active_data() {
|
||||
None => panic!("no tab is active"),
|
||||
Some(DemoView::TabA) => settings::view_column(vec![
|
||||
settings::section()
|
||||
.title("Debug")
|
||||
settings::view_section("Debug")
|
||||
.add(settings::item("Debug theme", choose_theme))
|
||||
.add(settings::item("Debug icon theme", choose_icon_theme))
|
||||
.add(settings::item(
|
||||
"Debug layout",
|
||||
toggler(window.debug).on_toggle(Message::Debug),
|
||||
toggler(None, window.debug, Message::Debug),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Scaling Factor",
|
||||
|
|
@ -277,11 +276,10 @@ impl State {
|
|||
.into(),
|
||||
]))
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Controls")
|
||||
settings::view_section("Controls")
|
||||
.add(settings::item(
|
||||
"Toggler",
|
||||
toggler(self.toggler_value).on_toggle(Message::TogglerToggled),
|
||||
toggler(None, self.toggler_value, Message::TogglerToggled),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Pick List (TODO)",
|
||||
|
|
@ -301,13 +299,15 @@ impl State {
|
|||
.add(settings::item(
|
||||
"Progress",
|
||||
progress_bar(0.0..=100.0, self.slider_value)
|
||||
.length(Length::Fixed(250.0))
|
||||
.girth(Length::Fixed(4.0)),
|
||||
.width(Length::Fixed(250.0))
|
||||
.height(Length::Fixed(4.0)),
|
||||
))
|
||||
.add(settings::item_row(vec![checkbox(self.checkbox_value)
|
||||
.label("Checkbox")
|
||||
.on_toggle(Message::CheckboxToggled)
|
||||
.into()]))
|
||||
.add(settings::item_row(vec![checkbox(
|
||||
"Checkbox",
|
||||
self.checkbox_value,
|
||||
Message::CheckboxToggled,
|
||||
)
|
||||
.into()]))
|
||||
.add(settings::item(
|
||||
format!(
|
||||
"Spin Button (Range {}:{})",
|
||||
|
|
@ -354,7 +354,8 @@ impl State {
|
|||
.width(Length::Shrink)
|
||||
.on_activate(Message::MultiSelection)
|
||||
.apply(container)
|
||||
.center_x(Length::Fill)
|
||||
.center_x()
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
text("Vertical With Spacing").into(),
|
||||
cosmic::iced::widget::row(vec![
|
||||
|
|
@ -423,12 +424,13 @@ impl State {
|
|||
])
|
||||
.padding(0)
|
||||
.into(),
|
||||
Some(DemoView::TabC) => settings::view_column(vec![settings::section()
|
||||
.title("Tab C")
|
||||
.add(text("Nothing here yet").width(Length::Fill))
|
||||
.into()])
|
||||
.padding(0)
|
||||
.into(),
|
||||
Some(DemoView::TabC) => {
|
||||
settings::view_column(vec![settings::view_section("Tab C")
|
||||
.add(text("Nothing here yet").width(Length::Fill))
|
||||
.into()])
|
||||
.padding(0)
|
||||
.into()
|
||||
}
|
||||
},
|
||||
container(text("Background container with some text").size(24))
|
||||
.layer(cosmic_theme::Layer::Background)
|
||||
|
|
|
|||
|
|
@ -147,8 +147,7 @@ impl State {
|
|||
fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
|
||||
settings::view_column(vec![
|
||||
window.parent_page_button(DesktopPage::DesktopOptions),
|
||||
settings::section()
|
||||
.title("Super Key Action")
|
||||
settings::view_section("Super Key Action")
|
||||
.add(settings::item("Launcher", horizontal_space(Length::Fill)))
|
||||
.add(settings::item("Workspaces", horizontal_space(Length::Fill)))
|
||||
.add(settings::item(
|
||||
|
|
@ -156,34 +155,38 @@ impl State {
|
|||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Hot Corner")
|
||||
settings::view_section("Hot Corner")
|
||||
.add(settings::item(
|
||||
"Enable top-left hot corner for Workspaces",
|
||||
toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner),
|
||||
toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner),
|
||||
))
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Top Panel")
|
||||
settings::view_section("Top Panel")
|
||||
.add(settings::item(
|
||||
"Show Workspaces Button",
|
||||
toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton),
|
||||
toggler(
|
||||
None,
|
||||
self.show_workspaces_button,
|
||||
Message::ShowWorkspacesButton,
|
||||
),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Show Applications Button",
|
||||
toggler(self.show_applications_button)
|
||||
.on_toggle(Message::ShowApplicationsButton),
|
||||
toggler(
|
||||
None,
|
||||
self.show_applications_button,
|
||||
Message::ShowApplicationsButton,
|
||||
),
|
||||
))
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Window Controls")
|
||||
settings::view_section("Window Controls")
|
||||
.add(settings::item(
|
||||
"Show Minimize Button",
|
||||
toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton),
|
||||
toggler(None, self.show_minimize_button, Message::ShowMinimizeButton),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Show Maximize Button",
|
||||
toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton),
|
||||
toggler(None, self.show_maximize_button, Message::ShowMaximizeButton),
|
||||
))
|
||||
.into(),
|
||||
])
|
||||
|
|
@ -242,12 +245,12 @@ impl State {
|
|||
list_column()
|
||||
.add(settings::item(
|
||||
"Same background on all displays",
|
||||
toggler(self.same_background).on_toggle(Message::SameBackground),
|
||||
toggler(None, self.same_background, Message::SameBackground),
|
||||
))
|
||||
.add(settings::item("Background fit", text("TODO")))
|
||||
.add(settings::item(
|
||||
"Slideshow",
|
||||
toggler(self.slideshow).on_toggle(Message::Slideshow),
|
||||
toggler(None, self.slideshow, Message::Slideshow),
|
||||
))
|
||||
.into(),
|
||||
column(image_column).spacing(16).into(),
|
||||
|
|
@ -258,8 +261,7 @@ impl State {
|
|||
fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
|
||||
settings::view_column(vec![
|
||||
window.parent_page_button(DesktopPage::Wallpaper),
|
||||
settings::section()
|
||||
.title("Workspace Behavior")
|
||||
settings::view_section("Workspace Behavior")
|
||||
.add(settings::item(
|
||||
"Dynamic workspaces",
|
||||
horizontal_space(Length::Fill),
|
||||
|
|
@ -269,8 +271,7 @@ impl State {
|
|||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Multi-monitor Behavior")
|
||||
settings::view_section("Multi-monitor Behavior")
|
||||
.add(settings::item(
|
||||
"Workspaces Span Displays",
|
||||
horizontal_space(Length::Fill),
|
||||
|
|
|
|||
|
|
@ -69,16 +69,14 @@ impl State {
|
|||
list_column()
|
||||
.add(settings::item("Device name", text("TODO")))
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Hardware")
|
||||
settings::view_section("Hardware")
|
||||
.add(settings::item("Hardware model", text("TODO")))
|
||||
.add(settings::item("Memory", text("TODO")))
|
||||
.add(settings::item("Processor", text("TODO")))
|
||||
.add(settings::item("Graphics", text("TODO")))
|
||||
.add(settings::item("Disk Capacity", text("TODO")))
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Operating System")
|
||||
settings::view_section("Operating System")
|
||||
.add(settings::item("Operating system", text("TODO")))
|
||||
.add(settings::item(
|
||||
"Operating system architecture",
|
||||
|
|
@ -87,8 +85,7 @@ impl State {
|
|||
.add(settings::item("Desktop environment", text("TODO")))
|
||||
.add(settings::item("Windowing system", text("TODO")))
|
||||
.into(),
|
||||
settings::section()
|
||||
.title("Related settings")
|
||||
settings::view_section("Related settings")
|
||||
.add(settings::item("Get support", text("TODO")))
|
||||
.into(),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
path = "../../"
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ impl cosmic::Application for App {
|
|||
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<'_, Self::Message> {
|
||||
let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12);
|
||||
let mut content = cosmic::widget::column().spacing(12);
|
||||
|
||||
for (id, image) in self.images.iter().enumerate() {
|
||||
content = content.push(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-log = "0.2.0"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ use std::collections::HashMap;
|
|||
use std::{env, process};
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||
use cosmic::iced::keyboard::Key;
|
||||
use cosmic::iced::window;
|
||||
use cosmic::iced::{Length, Size};
|
||||
use cosmic::iced_core::alignment::{Horizontal, Vertical};
|
||||
use cosmic::iced_core::keyboard::Key;
|
||||
use cosmic::iced_core::{Length, Size};
|
||||
use cosmic::widget::menu::action::MenuAction;
|
||||
use cosmic::widget::menu::key_bind::KeyBind;
|
||||
use cosmic::widget::menu::key_bind::Modifier;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::collections::HashMap;
|
|||
|
||||
use cosmic::{
|
||||
app::Core,
|
||||
iced::core::{id, Alignment, Length, Point},
|
||||
iced::widget::{column, container, scrollable, text},
|
||||
iced::{self, event, window, Subscription},
|
||||
iced_core::{id, Alignment, Length, Point},
|
||||
iced_widget::{column, container, scrollable, text},
|
||||
prelude::*,
|
||||
widget::{button, header_bar},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-log = "0.2.0"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::Size;
|
||||
use cosmic::iced_core::Size;
|
||||
use cosmic::widget::{menu, nav_bar};
|
||||
use cosmic::{executor, iced, ApplicationExt, Element};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ xdg-portal = ["libcosmic/xdg-portal"]
|
|||
|
||||
[dependencies]
|
||||
apply = "0.3.0"
|
||||
tokio = { version = "1.49", features = ["full"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
url = "2.5.8"
|
||||
tokio = { version = "1.44", features = ["full"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
url = "2.5.4"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
features = ["debug", "winit", "wgpu", "wayland", "tokio"]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
use apply::Apply;
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::dialog::file_chooser::{self, FileFilter};
|
||||
use cosmic::iced::Length;
|
||||
use cosmic::iced_core::Length;
|
||||
use cosmic::widget::button;
|
||||
use cosmic::{executor, iced, ApplicationExt, Element};
|
||||
use std::sync::Arc;
|
||||
|
|
@ -207,7 +207,7 @@ impl cosmic::Application for App {
|
|||
);
|
||||
|
||||
content.push(
|
||||
iced::widget::space::vertical()
|
||||
iced::widget::vertical_space()
|
||||
.height(Length::Fixed(12.0))
|
||||
.into(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
[package]
|
||||
name = "subscriptions"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dependencies.libcosmic]
|
||||
path = "../../"
|
||||
features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"]
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Application API example
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::Subscription;
|
||||
use cosmic::{executor, prelude::*, widget};
|
||||
|
||||
/// Runs application with these settings
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
cosmic::app::run::<App>(Settings::default(), ())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Messages that are used specifically by our [`App`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {}
|
||||
|
||||
/// The [`App`] stores application-specific state.
|
||||
pub struct App {
|
||||
core: Core,
|
||||
}
|
||||
|
||||
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||
impl cosmic::Application for App {
|
||||
/// Default async executor to use with the app.
|
||||
type Executor = executor::Default;
|
||||
|
||||
/// Argument received [`cosmic::Application::new`].
|
||||
type Flags = ();
|
||||
|
||||
/// Message type specific to our [`App`].
|
||||
type Message = Message;
|
||||
|
||||
/// The unique application ID to supply to the window manager.
|
||||
const APP_ID: &'static str = "org.cosmic.TextInputsDemo";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
/// Creates the application, and optionally emits task on initialize.
|
||||
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
|
||||
let mut app = App { core };
|
||||
|
||||
let commands = Task::batch(vec![app.update_title()]);
|
||||
|
||||
(app, commands)
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
Subscription::none()
|
||||
}
|
||||
|
||||
/// Handle application events here.
|
||||
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<'_, Self::Message> {
|
||||
widget::Row::new().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl App
|
||||
where
|
||||
Self: cosmic::Application,
|
||||
{
|
||||
fn update_title(&mut self) -> Task<Message> {
|
||||
let window_title = format!("COSMIC Subscriptions Demo");
|
||||
self.set_header_title(window_title.clone());
|
||||
self.set_window_title(window_title, self.core.main_window_id().unwrap())
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.17"
|
||||
tracing-log = "0.2.0"
|
||||
chrono = "*"
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::collections::HashMap;
|
|||
|
||||
use chrono::Datelike;
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::Size;
|
||||
use cosmic::iced_core::Size;
|
||||
use cosmic::prelude::*;
|
||||
use cosmic::widget::table;
|
||||
use cosmic::widget::{self, nav_bar};
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-log = "0.2.0"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
|
|
|
|||
|
|
@ -99,9 +99,7 @@ impl cosmic::Application for App {
|
|||
|
||||
let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input);
|
||||
|
||||
let column = cosmic::widget::column::with_capacity(2)
|
||||
.push(editable)
|
||||
.push(inline);
|
||||
let column = cosmic::widget::column().push(editable).push(inline);
|
||||
|
||||
let centered = cosmic::widget::container(column.width(200))
|
||||
.width(iced::Length::Fill)
|
||||
|
|
|
|||
|
|
@ -3,34 +3,8 @@ close = أغلِق
|
|||
# About
|
||||
license = الترخيص
|
||||
links = الروابط
|
||||
developers = المطوِّرون
|
||||
designers = المصمّمون
|
||||
developers = المطورون
|
||||
designers = المصممون
|
||||
artists = الفنانون
|
||||
translators = المترجمون
|
||||
documenters = الموثقون
|
||||
january = يناير { $year }
|
||||
february = فبراير { $year }
|
||||
march = مارس { $year }
|
||||
april = ابريل { $year }
|
||||
may = مايو { $year }
|
||||
june = يونيو { $year }
|
||||
july = يوليو { $year }
|
||||
august = أغسطس { $year }
|
||||
september = سبتمبر { $year }
|
||||
october = أكتوبر { $year }
|
||||
november = نوفمبر { $year }
|
||||
december = ديسمبر { $year }
|
||||
monday = الاثنين
|
||||
tuesday = الثلاثاء
|
||||
wednesday = الأربعاء
|
||||
thursday = الخميس
|
||||
friday = الجمعة
|
||||
saturday = السبت
|
||||
sunday = الأحد
|
||||
mon = ن
|
||||
tue = ث
|
||||
wed = ر
|
||||
thu = خ
|
||||
fri = ج
|
||||
sat = س
|
||||
sun = ح
|
||||
|
|
|
|||
|
|
@ -6,22 +6,3 @@ designers = Дызайнеры
|
|||
artists = Мастакі
|
||||
translators = Перакладчыкі
|
||||
documenters = Дакументалісты
|
||||
february = Люты { $year }
|
||||
november = Лістапад { $year }
|
||||
friday = Пт
|
||||
tuesday = Аў
|
||||
may = Май { $year }
|
||||
wednesday = Ср
|
||||
april = Красавік { $year }
|
||||
monday = Пн
|
||||
december = Снежань { $year }
|
||||
sunday = Нд
|
||||
march = Сакавік { $year }
|
||||
june = Чэрвень { $year }
|
||||
saturday = Сб
|
||||
august = Жнівень { $year }
|
||||
july = Ліпень { $year }
|
||||
thursday = Чц
|
||||
september = Верасень { $year }
|
||||
october = Кастрычнік { $year }
|
||||
january = Студзень { $year }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ designers = Designéři
|
|||
artists = Grafici
|
||||
translators = Překladatelé
|
||||
documenters = Tvůrci dokumentace
|
||||
sunday = Neděle
|
||||
sunday = Ne
|
||||
january = Leden { $year }
|
||||
february = Únor { $year }
|
||||
march = Březen { $year }
|
||||
|
|
@ -21,16 +21,9 @@ september = Září { $year }
|
|||
october = Říjen { $year }
|
||||
november = Listopad { $year }
|
||||
december = Prosinec { $year }
|
||||
monday = Pondělí
|
||||
tuesday = Úterý
|
||||
wednesday = Středa
|
||||
thursday = Čtvrtek
|
||||
friday = Pátek
|
||||
saturday = Sobota
|
||||
mon = Po
|
||||
tue = Út
|
||||
wed = St
|
||||
thu = Čt
|
||||
fri = Pá
|
||||
sat = So
|
||||
sun = Ne
|
||||
monday = Po
|
||||
tuesday = Út
|
||||
wednesday = St
|
||||
thursday = Čt
|
||||
friday = Pá
|
||||
saturday = So
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ close = Schließen
|
|||
# About
|
||||
license = Lizenz
|
||||
links = Links
|
||||
developers = Entwickler(innen)
|
||||
designers = Designer(innen)
|
||||
artists = Künstler(innen)
|
||||
translators = Übersetzer(innen)
|
||||
documenters = Dokumentierer(innen)
|
||||
developers = Entwickler*innen
|
||||
designers = Designer*innen
|
||||
artists = Künstler*innen
|
||||
translators = Übersetzer*innen
|
||||
documenters = Dokumentierer*innen
|
||||
# Calendar
|
||||
january = Januar { $year }
|
||||
february = Februar { $year }
|
||||
|
|
@ -21,17 +21,10 @@ september = September { $year }
|
|||
october = Oktober { $year }
|
||||
november = November { $year }
|
||||
december = Dezember { $year }
|
||||
monday = Montag
|
||||
tuesday = Dienstag
|
||||
wednesday = Mittwoch
|
||||
thursday = Donnerstag
|
||||
friday = Freitag
|
||||
saturday = Samstag
|
||||
sunday = Sonntag
|
||||
wed = Mi
|
||||
thu = Do
|
||||
fri = Fr
|
||||
sat = Sa
|
||||
sun = So
|
||||
tue = Di
|
||||
mon = Mo
|
||||
monday = Mo
|
||||
tuesday = Di
|
||||
wednesday = Mi
|
||||
thursday = Do
|
||||
friday = Fr
|
||||
saturday = Sa
|
||||
sunday = So
|
||||
|
|
|
|||
|
|
@ -23,17 +23,10 @@ september = September { $year }
|
|||
october = October { $year }
|
||||
november = November { $year }
|
||||
december = December { $year }
|
||||
monday = Monday
|
||||
mon = Mon
|
||||
tuesday = Tuesday
|
||||
tue = Tue
|
||||
wednesday = Wednesday
|
||||
wed = Wed
|
||||
thursday = Thursday
|
||||
thu = Thu
|
||||
friday = Friday
|
||||
fri = Fri
|
||||
saturday = Saturday
|
||||
sat = Sat
|
||||
sunday = Sunday
|
||||
sun = Sun
|
||||
monday = Mon
|
||||
tuesday = Tue
|
||||
wednesday = Wed
|
||||
thursday = Thu
|
||||
friday = Fri
|
||||
saturday = Sat
|
||||
sunday = Sun
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
monday = Maanantai
|
||||
mon = ma
|
||||
tuesday = Tiistai
|
||||
tue = ti
|
||||
wednesday = Keskiviikko
|
||||
wed = ke
|
||||
thursday = Torstai
|
||||
thu = to
|
||||
friday = Perjantai
|
||||
fri = pe
|
||||
saturday = Lauantai
|
||||
sat = la
|
||||
sunday = Sunnuntai
|
||||
sun = su
|
||||
close = Sulje
|
||||
license = Lisenssi
|
||||
links = Linkit
|
||||
developers = Kehittäjät
|
||||
designers = Suunnittelijat
|
||||
artists = Artistit
|
||||
translators = Kääntäjät
|
||||
documenters = Dokumentoijat
|
||||
january = Tammikuu { $year }
|
||||
february = Helmikuu { $year }
|
||||
march = Maaliskuu { $year }
|
||||
april = Huhtikuu { $year }
|
||||
may = Toukokuu { $year }
|
||||
june = Kesäkuu { $year }
|
||||
july = Heinäkuu { $year }
|
||||
august = Elokuu { $year }
|
||||
september = Syyskuu { $year }
|
||||
october = Lokakuu { $year }
|
||||
november = Marraskuu { $year }
|
||||
december = Joulukuu { $year }
|
||||
|
|
@ -10,25 +10,18 @@ february = Février { $year }
|
|||
april = Avril { $year }
|
||||
march = Mars { $year }
|
||||
november = Novembre { $year }
|
||||
friday = Vendredi
|
||||
tuesday = Mardi
|
||||
friday = Ven
|
||||
tuesday = Mar
|
||||
may = Mai { $year }
|
||||
wednesday = Mercredi
|
||||
monday = Lundi
|
||||
wednesday = Mer
|
||||
monday = Lun
|
||||
december = Décembre { $year }
|
||||
sunday = Dimanche
|
||||
sunday = Dim
|
||||
june = Juin { $year }
|
||||
saturday = Samedi
|
||||
saturday = Sam
|
||||
august = Août { $year }
|
||||
july = Juillet { $year }
|
||||
thursday = Jeudi
|
||||
thursday = Jeu
|
||||
september = Septembre { $year }
|
||||
october = Octobre { $year }
|
||||
designers = Designers
|
||||
mon = Lun
|
||||
tue = Mar
|
||||
wed = Mer
|
||||
thu = Jeu
|
||||
fri = Ven
|
||||
sat = Sam
|
||||
sun = Dim
|
||||
|
|
|
|||
|
|
@ -18,17 +18,10 @@ september = Meán Fómhair { $year }
|
|||
october = Deireadh Fómhair { $year }
|
||||
november = Samhain { $year }
|
||||
december = Nollaig { $year }
|
||||
monday = Dé Luain
|
||||
tuesday = Dé Máirt
|
||||
wednesday = Dé Céadaoin
|
||||
thursday = Déardaoin
|
||||
friday = Dé hAoine
|
||||
saturday = Dé Sathairn
|
||||
sunday = Dé Domhnaigh
|
||||
mon = Lua
|
||||
tue = Mái
|
||||
wed = Céa
|
||||
thu = Déa
|
||||
fri = Aoi
|
||||
sat = Sat
|
||||
sun = Dom
|
||||
monday = Lua
|
||||
tuesday = Mái
|
||||
wednesday = Céa
|
||||
thursday = Déa
|
||||
friday = Aoi
|
||||
saturday = Sat
|
||||
sunday = Dom
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
close = Bezárás
|
||||
# About
|
||||
license = Licenc
|
||||
links = Hivatkozások
|
||||
links = Linkek
|
||||
developers = Fejlesztők
|
||||
designers = Tervezők
|
||||
artists = Művészek
|
||||
|
|
@ -20,17 +20,10 @@ september = { $year } szeptember
|
|||
october = { $year } október
|
||||
november = { $year } november
|
||||
december = { $year } december
|
||||
monday = Hétfő
|
||||
tuesday = Kedd
|
||||
wednesday = Szerda
|
||||
thursday = Csütörtök
|
||||
friday = Péntek
|
||||
saturday = Szombat
|
||||
sunday = Vasárnap
|
||||
mon = H
|
||||
tue = K
|
||||
wed = Sze
|
||||
thu = Cs
|
||||
fri = P
|
||||
sat = Szo
|
||||
sun = V
|
||||
monday = H
|
||||
tuesday = K
|
||||
wednesday = Sze
|
||||
thursday = Cs
|
||||
friday = P
|
||||
saturday = Szo
|
||||
sunday = V
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
close = Tutup
|
||||
license = Lisensi
|
||||
links = Tautan
|
||||
developers = Pengembang
|
||||
designers = Perancang
|
||||
artists = Artis
|
||||
translators = Penerjemah
|
||||
documenters = Dokumenter
|
||||
january = Januari { $year }
|
||||
february = Februari { $year }
|
||||
march = Maret { $year }
|
||||
april = April { $year }
|
||||
may = Mei { $year }
|
||||
june = Juni { $year }
|
||||
july = Juli { $year }
|
||||
august = Agustus { $year }
|
||||
september = September { $year }
|
||||
october = Oktober { $year }
|
||||
november = November { $year }
|
||||
december = Desember { $year }
|
||||
monday = Senin
|
||||
tuesday = Selasa
|
||||
wednesday = Rabu
|
||||
sunday = Minggu
|
||||
saturday = Sabtu
|
||||
friday = Jum'at
|
||||
thursday = Kamis
|
||||
mon = Sen
|
||||
tue = Sel
|
||||
wed = Rab
|
||||
thu = Kam
|
||||
fri = Jum
|
||||
sat = Sab
|
||||
sun = Min
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
close = Mdel
|
||||
license = Turagt
|
||||
links = Iseɣwan
|
||||
developers = Ineflayen
|
||||
artists = Inaẓuren
|
||||
translators = Imsuqlen
|
||||
january = Yennayer { $year }
|
||||
february = Fuṛar { $year }
|
||||
march = Meɣres { $year }
|
||||
april = Yebrir { $year }
|
||||
may = Mayyu { $year }
|
||||
june = Yunyu { $year }
|
||||
july = Yulyu { $year }
|
||||
august = Ɣuct { $year }
|
||||
september = Ctembeṛ { $year }
|
||||
october = Tubeṛ { $year }
|
||||
november = Wambeṛ { $year }
|
||||
december = Dujembeṛ { $year }
|
||||
documenters = Imeskaren
|
||||
monday = Arim
|
||||
mon = Ari
|
||||
tuesday = Aram
|
||||
tue = Ara
|
||||
wednesday = Ahad
|
||||
wed = Aha
|
||||
thursday = Amhad
|
||||
thu = Amh
|
||||
friday = Sem
|
||||
fri = Sm
|
||||
saturday = Sed
|
||||
sat = Sd
|
||||
sunday = Acer
|
||||
sun = Ace
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
close = Жабу
|
||||
license = Лицензия
|
||||
links = Сілтемелер
|
||||
developers = Әзірлеушілер
|
||||
designers = Дизайнерлер
|
||||
artists = Суретшілер
|
||||
translators = Аудармашылар
|
||||
documenters = Құжаттаушылар
|
||||
january = Қаңтар { $year }
|
||||
february = Ақпан { $year }
|
||||
march = Наурыз { $year }
|
||||
april = Сәуір { $year }
|
||||
may = Мамыр { $year }
|
||||
june = Маусым { $year }
|
||||
july = Шілде { $year }
|
||||
august = Тамыз { $year }
|
||||
september = Қыркүйек { $year }
|
||||
october = Қазан { $year }
|
||||
november = Қараша { $year }
|
||||
december = Желтоқсан { $year }
|
||||
monday = Дүйсенбі
|
||||
tuesday = Сейсенбі
|
||||
wednesday = Сәрсенбі
|
||||
thursday = Бейсенбі
|
||||
friday = Жұма
|
||||
saturday = Сенбі
|
||||
sunday = Жексенбі
|
||||
mon = Дс
|
||||
tue = Сс
|
||||
wed = Ср
|
||||
thu = Бс
|
||||
fri = Жм
|
||||
sat = Сн
|
||||
sun = Жк
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
february = { $year }년 2월
|
||||
close = 닫기
|
||||
documenters = 문서 작성자
|
||||
november = { $year }년 11월
|
||||
friday = 금요일
|
||||
tuesday = 화요일
|
||||
may = { $year }년 5월
|
||||
wednesday = 수요일
|
||||
april = { $year }년 4월
|
||||
monday = 월요일
|
||||
translators = 번역가
|
||||
artists = 아티스트
|
||||
license = 라이선스
|
||||
december = { $year }년 12월
|
||||
sunday = 일요일
|
||||
links = 링크
|
||||
march = { $year }년 3월
|
||||
june = { $year }년 6월
|
||||
saturday = 토요일
|
||||
august = { $year }년 8월
|
||||
developers = 개발자
|
||||
july = { $year }년 7월
|
||||
thursday = 목요일
|
||||
september = { $year }년 9월
|
||||
designers = 디자이너
|
||||
october = { $year }년 10월
|
||||
january = { $year }년 1월
|
||||
mon = 월
|
||||
tue = 화
|
||||
wed = 수
|
||||
thu = 목
|
||||
fri = 금
|
||||
sat = 토
|
||||
sun = 일
|
||||
|
|
@ -2,33 +2,26 @@ february = Vasaris { $year }
|
|||
close = Uždaryti
|
||||
documenters = Dokumentuotojai
|
||||
november = Lapkritis { $year }
|
||||
friday = Penktadienis
|
||||
tuesday = Antradienis
|
||||
friday = Penk
|
||||
tuesday = Antr
|
||||
may = Gegužė { $year }
|
||||
wednesday = Trečiadienis
|
||||
wednesday = Treč
|
||||
april = Balandis { $year }
|
||||
monday = Pirmadienis
|
||||
monday = Pirm
|
||||
translators = Vertėjai
|
||||
artists = Menininkai
|
||||
license = Licencija
|
||||
december = Gruodis { $year }
|
||||
sunday = Sekmadienis
|
||||
sunday = Sekm
|
||||
links = Nuorodos
|
||||
march = Kovas { $year }
|
||||
june = Birželis { $year }
|
||||
saturday = Šeštadienis
|
||||
saturday = Šešt
|
||||
august = Rugpjūtis { $year }
|
||||
developers = Kūrėjai
|
||||
july = Liepa { $year }
|
||||
thursday = Ketvirtadienis
|
||||
thursday = Ketv
|
||||
september = Rugsėjis { $year }
|
||||
designers = Dizaineriai
|
||||
october = Spalis { $year }
|
||||
january = Sausis { $year }
|
||||
mon = Pirm
|
||||
tue = Antr
|
||||
wed = Treč
|
||||
thu = Ketv
|
||||
fri = Penkt
|
||||
sat = Šešt
|
||||
sun = Sekm
|
||||
|
|
|
|||
|
|
@ -18,10 +18,4 @@ wednesday = Woe
|
|||
thursday = Do
|
||||
friday = Vrij
|
||||
saturday = Za
|
||||
sunday = Zo
|
||||
links = Links
|
||||
developers = Ontwikkeling
|
||||
designers = Ontwerp
|
||||
translators = Vertaling
|
||||
documenters = Documentatie
|
||||
artists = Vormgeving
|
||||
sunday = Zon
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
close = ਬੰਦ ਕਰੋ
|
||||
license = ਲਸੰਸ
|
||||
links = ਲਿੰਕ
|
||||
developers = ਡਿਵੈਲਪਰ
|
||||
designers = ਡਿਜ਼ਾਇਨਰ
|
||||
artists = ਕਲਾਕਾਰ
|
||||
translators = ਅਨੁਵਾਦਕ
|
||||
documenters = ਦਸਤਾਵੇਜ਼ ਤਿਆਰ ਕਰਤਾ
|
||||
january = ਜਨਵਰੀ { $year }
|
||||
february = ਫਰਵਰੀ { $year }
|
||||
march = ਮਾਰਚ { $year }
|
||||
april = ਅਪਰੈਲ { $year }
|
||||
may = ਮਈ { $year }
|
||||
june = ਜੂਨ { $year }
|
||||
july = ਜੁਲਾਈ { $year }
|
||||
august = ਅਗਸਤ { $year }
|
||||
september = ਸਤੰਬਰ { $year }
|
||||
october = ਅਕਤੂਬਰ { $year }
|
||||
november = ਨਵੰਬਰ { $year }
|
||||
december = ਦਸੰਬਰ { $year }
|
||||
monday = ਸੋਮਵਾਰ
|
||||
mon = ਸੋਮ
|
||||
tuesday = ਮੰਗਲਵਾਰ
|
||||
tue = ਮੰਗਲ
|
||||
wednesday = ਬੁੱਧਵਾਰ
|
||||
wed = ਬੁੱਧ
|
||||
thursday = ਵੀਰਵਾਰ
|
||||
thu = ਵੀਰ
|
||||
friday = ਸ਼ੁੱਕਰਵਾਰ
|
||||
fri = ਸ਼ੁੱਕਰ
|
||||
saturday = ਸ਼ਨਿੱਚਰਵਾਰ
|
||||
sat = ਸ਼ਨਿੱਚਰ
|
||||
sunday = ਐਤਵਾਰ
|
||||
sun = ਐਤ
|
||||
|
|
@ -20,17 +20,10 @@ september = Wrzesień { $year }
|
|||
october = Październik { $year }
|
||||
november = Listopad { $year }
|
||||
december = Grudzień { $year }
|
||||
monday = Poniedziałek
|
||||
tuesday = Wtorek
|
||||
wednesday = Środa
|
||||
thursday = Czwartek
|
||||
friday = Piątek
|
||||
saturday = Sobota
|
||||
sunday = Niedziela
|
||||
mon = Pon
|
||||
tue = Wto
|
||||
wed = Śro
|
||||
thu = Czw
|
||||
fri = Pia
|
||||
sat = Sob
|
||||
sun = Nie
|
||||
monday = Pon
|
||||
tuesday = Wto
|
||||
wednesday = Śro
|
||||
thursday = Czw
|
||||
friday = Pią
|
||||
saturday = Sob
|
||||
sunday = Nie
|
||||
|
|
|
|||
|
|
@ -8,29 +8,22 @@ designers = Designers
|
|||
artists = Artistas
|
||||
translators = Tradutores
|
||||
documenters = Documentadores
|
||||
january = Janeiro de { $year }
|
||||
february = Fevereiro de { $year }
|
||||
march = Março de { $year }
|
||||
april = Abril de { $year }
|
||||
may = Maio de { $year }
|
||||
june = Junho de { $year }
|
||||
july = Julho de { $year }
|
||||
august = Agosto de { $year }
|
||||
september = Setembro de { $year }
|
||||
october = Outubro de { $year }
|
||||
november = Novembro de { $year }
|
||||
december = Dezembro de { $year }
|
||||
monday = Segunda-feira
|
||||
tuesday = Terça-feira
|
||||
wednesday = Quarta-feira
|
||||
thursday = Quinta-feira
|
||||
friday = Sexta-feira
|
||||
saturday = Sábado
|
||||
sunday = Domingo
|
||||
mon = Seg
|
||||
tue = Ter
|
||||
wed = Qua
|
||||
thu = Qui
|
||||
fri = Sex
|
||||
sat = Sáb
|
||||
sun = Dom
|
||||
january = Janeiro { $year }
|
||||
february = Fevereiro { $year }
|
||||
march = Março { $year }
|
||||
april = Abril { $year }
|
||||
may = Maio { $year }
|
||||
june = Junho { $year }
|
||||
july = Julho { $year }
|
||||
august = Agosto { $year }
|
||||
september = Setembro { $year }
|
||||
october = Outubro { $year }
|
||||
november = Novembro { $year }
|
||||
december = Dezembro { $year }
|
||||
monday = Seg
|
||||
tuesday = Ter
|
||||
wednesday = Qua
|
||||
thursday = Qui
|
||||
friday = Sex
|
||||
saturday = Sáb
|
||||
sunday = Dom
|
||||
|
|
|
|||
|
|
@ -6,29 +6,3 @@ designers = Дизайнеры
|
|||
artists = Художники
|
||||
translators = Переводчики
|
||||
documenters = Авторы документации
|
||||
january = Январь { $year }
|
||||
february = Февраль { $year }
|
||||
march = Март { $year }
|
||||
april = Апрель { $year }
|
||||
may = Май { $year }
|
||||
june = Июнь { $year }
|
||||
july = Июль { $year }
|
||||
august = Август { $year }
|
||||
september = Сентябрь { $year }
|
||||
october = Октябрь { $year }
|
||||
november = Ноябрь { $year }
|
||||
december = Декабрь { $year }
|
||||
monday = Понедельник
|
||||
tuesday = Вторник
|
||||
wednesday = Среда
|
||||
thursday = Четверг
|
||||
friday = Пятница
|
||||
saturday = Суббота
|
||||
sunday = Воскресенье
|
||||
mon = Пн
|
||||
tue = Вт
|
||||
wed = Ср
|
||||
thu = Чт
|
||||
fri = Пт
|
||||
sat = Сб
|
||||
sun = Вс
|
||||
|
|
|
|||
|
|
@ -18,17 +18,10 @@ september = September { $year }
|
|||
october = Oktober { $year }
|
||||
november = November { $year }
|
||||
december = December { $year }
|
||||
monday = Måndag
|
||||
tuesday = Tisdag
|
||||
wednesday = Onsdag
|
||||
thursday = Torsdag
|
||||
friday = Fredag
|
||||
saturday = Lördag
|
||||
sunday = Söndag
|
||||
sun = Sön
|
||||
mon = Mån
|
||||
tue = Tis
|
||||
wed = Ons
|
||||
thu = Tor
|
||||
fri = Fre
|
||||
sat = Lör
|
||||
monday = Mån
|
||||
tuesday = Tis
|
||||
wednesday = Ons
|
||||
thursday = Tor
|
||||
friday = Fre
|
||||
saturday = Lör
|
||||
sunday = Sön
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Context Drawer
|
||||
close = Kapat
|
||||
|
||||
# About
|
||||
license = Lisans
|
||||
links = Bağlantılar
|
||||
|
|
@ -8,29 +9,3 @@ designers = Tasarımcılar
|
|||
artists = Sanatçılar
|
||||
translators = Çevirmenler
|
||||
documenters = Belgelendiriciler
|
||||
january = Ocak { $year }
|
||||
february = Şubat { $year }
|
||||
march = Mart { $year }
|
||||
april = Nisan { $year }
|
||||
may = Mayıs { $year }
|
||||
june = Haziran { $year }
|
||||
july = Temmuz { $year }
|
||||
august = Ağustos { $year }
|
||||
september = Eylül { $year }
|
||||
october = Ekim { $year }
|
||||
november = Kasım { $year }
|
||||
december = Aralık { $year }
|
||||
monday = Pazartesi
|
||||
mon = Pzt
|
||||
tuesday = Salı
|
||||
tue = Sal
|
||||
wednesday = Çarşamba
|
||||
wed = Çar
|
||||
thursday = Perşembe
|
||||
thu = Per
|
||||
friday = Cuma
|
||||
fri = Cum
|
||||
saturday = Cumartesi
|
||||
sat = Cmt
|
||||
sunday = Pazar
|
||||
sun = Paz
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
close = Закрити
|
||||
# About
|
||||
license = Ліцензія
|
||||
links = Ланки
|
||||
links = Посилання
|
||||
developers = Розробники
|
||||
designers = Дизайнери
|
||||
artists = Художники
|
||||
|
|
@ -10,27 +10,20 @@ translators = Перекладачі
|
|||
documenters = Документатори
|
||||
february = Лютий { $year }
|
||||
november = Листопад { $year }
|
||||
friday = П'ятниця
|
||||
tuesday = Вівторок
|
||||
friday = Пт
|
||||
tuesday = Вт
|
||||
may = Травень { $year }
|
||||
wednesday = Середа
|
||||
wednesday = Ср
|
||||
april = Квітень { $year }
|
||||
monday = Понеділок
|
||||
monday = Пн
|
||||
december = Грудень { $year }
|
||||
sunday = Неділя
|
||||
sunday = Нд
|
||||
march = Березень { $year }
|
||||
june = Червень { $year }
|
||||
saturday = Субота
|
||||
saturday = Сб
|
||||
august = Серпень { $year }
|
||||
july = Липень { $year }
|
||||
thursday = Четвер
|
||||
thursday = Чт
|
||||
september = Вересень { $year }
|
||||
october = Жовтень { $year }
|
||||
january = Січень { $year }
|
||||
mon = Пн
|
||||
tue = Вт
|
||||
wed = Ср
|
||||
thu = Чт
|
||||
fri = Пт
|
||||
sat = Cб
|
||||
sun = Нд
|
||||
|
|
|
|||
|
|
@ -4,31 +4,3 @@ links = 链接
|
|||
developers = 开发者
|
||||
designers = 设计师
|
||||
translators = 译者
|
||||
january = { $year }年1月
|
||||
february = { $year }年2月
|
||||
march = { $year }年3月
|
||||
april = { $year }年4月
|
||||
may = { $year }年5月
|
||||
june = { $year }年6月
|
||||
july = { $year }年7月
|
||||
august = { $year }年8月
|
||||
september = { $year }年9月
|
||||
october = { $year }年10月
|
||||
november = { $year }年11月
|
||||
december = { $year }年12月
|
||||
monday = 星期一
|
||||
tuesday = 星期二
|
||||
wednesday = 星期三
|
||||
thursday = 星期四
|
||||
friday = 星期五
|
||||
saturday = 星期六
|
||||
sunday = 星期日
|
||||
artists = 艺术家
|
||||
documenters = 文档作者
|
||||
mon = 周一
|
||||
tue = 周二
|
||||
wed = 周三
|
||||
thu = 周四
|
||||
fri = 周五
|
||||
sat = 周六
|
||||
sun = 周日
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
close = 關閉
|
||||
developers = 開發人員
|
||||
designers = 設計人員
|
||||
artists = 美編設計
|
||||
translators = 翻譯人員
|
||||
documenters = 文件編輯人員
|
||||
january = { $year } 年 1 月
|
||||
monday = 星期一
|
||||
tuesday = 星期二
|
||||
wednesday = 星期三
|
||||
thursday = 星期四
|
||||
friday = 星期五
|
||||
saturday = 星期六
|
||||
sunday = 星期日
|
||||
mon = 週一
|
||||
tue = 週二
|
||||
wed = 週三
|
||||
thu = 週四
|
||||
fri = 週五
|
||||
sat = 週六
|
||||
sun = 週日
|
||||
license = 授權
|
||||
links = 連結
|
||||
february = { $year } 年 2 月
|
||||
march = { $year } 年 3 月
|
||||
april = { $year } 年 4 月
|
||||
may = { $year } 年 5 月
|
||||
june = { $year } 年 6 月
|
||||
july = { $year } 年 7 月
|
||||
august = { $year } 年 8 月
|
||||
september = { $year } 年 9 月
|
||||
october = { $year } 年 10 月
|
||||
november = { $year } 年 11 月
|
||||
december = { $year } 年 12 月
|
||||
2
iced
2
iced
|
|
@ -1 +1 @@
|
|||
Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece
|
||||
Subproject commit 10db38f982001a714bd94e99a082368762b378ee
|
||||
51
src/anim.rs
51
src/anim.rs
|
|
@ -1,51 +0,0 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A simple linear interpolation calculation function.
|
||||
/// p = `percent_complete` in decimal form
|
||||
#[must_use]
|
||||
pub fn lerp(start: f32, end: f32, p: f32) -> f32 {
|
||||
(1.0 - p) * start + p * end
|
||||
}
|
||||
|
||||
/// A fast smooth interpolation calculation function.
|
||||
/// p = `percent_complete` in decimal form
|
||||
#[must_use]
|
||||
pub fn slerp(start: f32, end: f32, p: f32) -> f32 {
|
||||
let t = smootherstep(p);
|
||||
(1.0 - t) * start + t * end
|
||||
}
|
||||
|
||||
/// utility function which maps a value [0, 1] -> [0, 1] using the smootherstep function
|
||||
pub fn smootherstep(t: f32) -> f32 {
|
||||
(6.0 * t.powi(5) - 15.0 * t.powi(4) + 10.0 * t.powi(3)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct State {
|
||||
pub last_change: Option<Instant>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn changed(&mut self, dur: Duration) {
|
||||
let t = self.t(dur, false);
|
||||
let diff = dur.mul_f32(t.abs());
|
||||
let now = Instant::now();
|
||||
self.last_change = Some(now.checked_sub(diff).unwrap_or(now));
|
||||
}
|
||||
|
||||
pub fn anim_done(&mut self, dur: Duration) {
|
||||
if self
|
||||
.last_change
|
||||
.is_some_and(|t| Instant::now().duration_since(t) > dur)
|
||||
{
|
||||
self.last_change = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn t(&self, dur: Duration, forward: bool) -> f32 {
|
||||
let res = self.last_change.map_or(1., |t| {
|
||||
Instant::now().duration_since(t).as_millis() as f32 / dur.as_millis() as f32
|
||||
});
|
||||
if forward { res } else { 1. - res }
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,11 @@ use crate::surface;
|
|||
use crate::theme::Theme;
|
||||
use crate::widget::nav_bar;
|
||||
use crate::{config::CosmicTk, keyboard_nav};
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
|
||||
use cosmic_theme::ThemeMode;
|
||||
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
|
||||
use iced::Application as IcedApplication;
|
||||
|
||||
/// A message managed internally by COSMIC.
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -69,10 +71,10 @@ pub enum Action {
|
|||
/// Updates the tracked window geometry.
|
||||
WindowResize(iced::window::Id, f32, f32),
|
||||
/// Tracks updates to window state.
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
WindowState(iced::window::Id, WindowState),
|
||||
/// Capabilities the window manager supports
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
WmCapabilities(iced::window::Id, WindowManagerCapabilities),
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
DesktopSettings(crate::theme::portal::Desktop),
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ use std::sync::Arc;
|
|||
use super::{Action, Application, ApplicationExt, Subscription};
|
||||
use crate::theme::{THEME, Theme, ThemeType};
|
||||
use crate::{Core, Element, keyboard_nav};
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
|
||||
use cosmic_theme::ThemeMode;
|
||||
#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))]
|
||||
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
|
||||
use iced::Application as IcedApplication;
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
use iced::event::wayland;
|
||||
use iced::{Task, theme, window};
|
||||
use iced::{Task, window};
|
||||
use iced_futures::event::listen_with;
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
use iced_winit::SurfaceIdWrapper;
|
||||
use palette::color_difference::EuclideanDistance;
|
||||
|
||||
|
|
@ -49,8 +49,8 @@ pub fn windowing_system() -> Option<WindowingSystem> {
|
|||
WINDOWING_SYSTEM.get().copied()
|
||||
}
|
||||
|
||||
fn init_windowing_system<M>(handle: window::raw_window_handle::WindowHandle) -> crate::Action<M> {
|
||||
let raw = handle.as_ref();
|
||||
fn init_windowing_system<M>(handle: raw_window_handle::WindowHandle) -> crate::Action<M> {
|
||||
let raw: &raw_window_handle::RawWindowHandle = handle.as_ref();
|
||||
let system = match raw {
|
||||
window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit,
|
||||
window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit,
|
||||
|
|
@ -83,7 +83,7 @@ fn init_windowing_system<M>(handle: window::raw_window_handle::WindowHandle) ->
|
|||
#[derive(Default)]
|
||||
pub struct Cosmic<App: Application> {
|
||||
pub app: App,
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
pub surface_views: HashMap<
|
||||
window::Id,
|
||||
(
|
||||
|
|
@ -138,7 +138,7 @@ where
|
|||
) -> iced::Task<crate::Action<T::Message>> {
|
||||
#[cfg(feature = "surface-message")]
|
||||
match _surface_message {
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::AppSubsurface(settings, view) => {
|
||||
let Some(settings) = std::sync::Arc::try_unwrap(settings)
|
||||
.ok()
|
||||
|
|
@ -168,7 +168,7 @@ where
|
|||
iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app))
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::Subsurface(settings, view) => {
|
||||
let Some(settings) = std::sync::Arc::try_unwrap(settings)
|
||||
.ok()
|
||||
|
|
@ -196,7 +196,7 @@ where
|
|||
iced_winit::commands::subsurface::get_subsurface(settings())
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::AppPopup(settings, view) => {
|
||||
let Some(settings) = std::sync::Arc::try_unwrap(settings)
|
||||
.ok()
|
||||
|
|
@ -225,26 +225,15 @@ where
|
|||
iced_winit::commands::popup::get_popup(settings(&mut self.app))
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::DestroyPopup(id) => {
|
||||
iced_winit::commands::popup::destroy_popup(id)
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
crate::surface::Action::DestroyTooltipPopup => {
|
||||
#[cfg(feature = "applet")]
|
||||
{
|
||||
iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID)
|
||||
}
|
||||
#[cfg(not(feature = "applet"))]
|
||||
{
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::DestroySubsurface(id) => {
|
||||
iced_winit::commands::subsurface::destroy_subsurface(id)
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::DestroyWindow(id) => iced::window::close(id),
|
||||
crate::surface::Action::ResponsiveMenuBar {
|
||||
menu_bar,
|
||||
|
|
@ -255,7 +244,7 @@ where
|
|||
core.menu_bars.insert(menu_bar, (limits, size));
|
||||
iced::Task::none()
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::Popup(settings, view) => {
|
||||
let Some(settings) = std::sync::Arc::try_unwrap(settings)
|
||||
.ok()
|
||||
|
|
@ -282,7 +271,7 @@ where
|
|||
iced_winit::commands::popup::get_popup(settings())
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::AppWindow(id, settings, view) => {
|
||||
let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| {
|
||||
s.downcast::<Box<dyn Fn(&mut T) -> iced::window::Settings + Send + Sync>>()
|
||||
|
|
@ -321,7 +310,7 @@ where
|
|||
.discard()
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
crate::surface::Action::Window(id, settings, view) => {
|
||||
let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| {
|
||||
s.downcast::<Box<dyn Fn() -> iced::window::Settings + Send + Sync>>()
|
||||
|
|
@ -380,16 +369,7 @@ where
|
|||
crate::Action::Cosmic(message) => self.cosmic_update(message),
|
||||
crate::Action::None => iced::Task::none(),
|
||||
#[cfg(feature = "single-instance")]
|
||||
crate::Action::DbusActivation(message) => {
|
||||
let mut task = self.app.dbus_activation(message);
|
||||
|
||||
if let Some(id) = self.app.core().main_window_id() {
|
||||
let unminimize = iced_runtime::window::minimize::<()>(id, false);
|
||||
task = task.chain(unminimize.discard());
|
||||
}
|
||||
|
||||
task
|
||||
}
|
||||
crate::Action::DbusActivation(message) => self.app.dbus_activation(message),
|
||||
};
|
||||
|
||||
#[cfg(all(target_env = "gnu", not(target_os = "windows")))]
|
||||
|
|
@ -408,16 +388,15 @@ where
|
|||
f64::from(self.app.core().scale_factor())
|
||||
}
|
||||
|
||||
pub fn style(&self, theme: &Theme) -> theme::Style {
|
||||
pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance {
|
||||
if let Some(style) = self.app.style() {
|
||||
style
|
||||
} else if self.app.core().window.is_maximized {
|
||||
let theme = THEME.lock().unwrap();
|
||||
crate::style::iced::application::style(theme.borrow())
|
||||
crate::style::iced::application::appearance(theme.borrow())
|
||||
} else {
|
||||
let theme = THEME.lock().unwrap();
|
||||
|
||||
theme::Style {
|
||||
iced_runtime::Appearance {
|
||||
background_color: iced_core::Color::TRANSPARENT,
|
||||
icon_color: theme.cosmic().on_bg_color().into(),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
|
|
@ -441,7 +420,7 @@ where
|
|||
}
|
||||
iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)),
|
||||
iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)),
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
|
||||
match event {
|
||||
wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
|
||||
|
|
@ -454,7 +433,7 @@ where
|
|||
) => {
|
||||
return Some(Action::SuggestedBounds(b));
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState(
|
||||
s,
|
||||
)) => {
|
||||
|
|
@ -571,7 +550,7 @@ where
|
|||
|
||||
#[cfg(feature = "multi-window")]
|
||||
pub fn view(&self, id: window::Id) -> Element<'_, crate::Action<T::Message>> {
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
if let Some((_, _, v)) = self.surface_views.get(&id) {
|
||||
return v(&self.app);
|
||||
}
|
||||
|
|
@ -622,7 +601,7 @@ impl<T: Application> Cosmic<T> {
|
|||
fn cosmic_update(&mut self, message: Action) -> iced::Task<crate::Action<T::Message>> {
|
||||
match message {
|
||||
Action::WindowMaximized(id, maximized) => {
|
||||
#[cfg(not(all(feature = "wayland", target_os = "linux")))]
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
if self
|
||||
.app
|
||||
.core()
|
||||
|
|
@ -647,12 +626,12 @@ impl<T: Application> Cosmic<T> {
|
|||
self.app.on_window_resize(id, width, height);
|
||||
|
||||
//TODO: more efficient test of maximized (winit has no event for maximize if set by the OS)
|
||||
return iced::window::is_maximized(id).map(move |maximized| {
|
||||
return iced::window::get_maximized(id).map(move |maximized| {
|
||||
crate::Action::Cosmic(Action::WindowMaximized(id, maximized))
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
Action::WindowState(id, state) => {
|
||||
if self
|
||||
.app
|
||||
|
|
@ -704,7 +683,7 @@ impl<T: Application> Cosmic<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
Action::WmCapabilities(id, capabilities) => {
|
||||
if self
|
||||
.app
|
||||
|
|
@ -723,10 +702,10 @@ impl<T: Application> Cosmic<T> {
|
|||
|
||||
Action::KeyboardNav(message) => match message {
|
||||
keyboard_nav::Action::FocusNext => {
|
||||
return iced::widget::operation::focus_next().map(crate::Action::Cosmic);
|
||||
return iced::widget::focus_next().map(crate::Action::Cosmic);
|
||||
}
|
||||
keyboard_nav::Action::FocusPrevious => {
|
||||
return iced::widget::operation::focus_previous().map(crate::Action::Cosmic);
|
||||
return iced::widget::focus_previous().map(crate::Action::Cosmic);
|
||||
}
|
||||
keyboard_nav::Action::Escape => return self.app.on_escape(),
|
||||
keyboard_nav::Action::Search => return self.app.on_search(),
|
||||
|
|
@ -811,7 +790,7 @@ impl<T: Application> Cosmic<T> {
|
|||
new_theme.theme_type.prefer_dark(prefer_dark);
|
||||
|
||||
cosmic_theme.set_theme(new_theme.theme_type);
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
if self.app.core().sync_window_border_radii_to_theme() {
|
||||
use iced_runtime::platform_specific::wayland::CornerRadius;
|
||||
use iced_winit::platform_specific::commands::corner_radius::corner_radius;
|
||||
|
|
@ -957,7 +936,7 @@ impl<T: Application> Cosmic<T> {
|
|||
// Only apply update if the theme is set to load a system theme
|
||||
if let ThemeType::System { .. } = cosmic_theme.theme_type {
|
||||
cosmic_theme.set_theme(new_theme.theme_type);
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
if self.app.core().sync_window_border_radii_to_theme() {
|
||||
use iced_runtime::platform_specific::wayland::CornerRadius;
|
||||
use iced_winit::platform_specific::commands::corner_radius::corner_radius;
|
||||
|
|
@ -1046,28 +1025,15 @@ impl<T: Application> Cosmic<T> {
|
|||
}
|
||||
return Task::batch(cmds);
|
||||
}
|
||||
Action::Activate(_token) => {
|
||||
Action::Activate(_token) =>
|
||||
{
|
||||
#[cfg(feature = "wayland")]
|
||||
if let Some(id) = self.app.core().main_window_id() {
|
||||
// Unminimize window before requesting to activate it.
|
||||
let mut task = iced_runtime::window::minimize(id, false);
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
{
|
||||
task = task.chain(
|
||||
iced_winit::platform_specific::commands::activation::activate(
|
||||
id,
|
||||
#[allow(clippy::used_underscore_binding)]
|
||||
_token,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "wayland", target_os = "linux")))]
|
||||
{
|
||||
task = task.chain(iced_runtime::window::gain_focus(id));
|
||||
}
|
||||
|
||||
return task;
|
||||
return iced_winit::platform_specific::commands::activation::activate(
|
||||
id,
|
||||
#[allow(clippy::used_underscore_binding)]
|
||||
_token,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1079,7 +1045,7 @@ impl<T: Application> Cosmic<T> {
|
|||
*v == 0
|
||||
}) {
|
||||
self.opened_surfaces.remove(&id);
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
self.surface_views.remove(&id);
|
||||
self.tracked_windows.remove(&id);
|
||||
}
|
||||
|
|
@ -1201,8 +1167,7 @@ impl<T: Application> Cosmic<T> {
|
|||
#[cfg(all(
|
||||
feature = "wayland",
|
||||
feature = "multi-window",
|
||||
feature = "surface-message",
|
||||
target_os = "linux"
|
||||
feature = "surface-message"
|
||||
))]
|
||||
if let Some((
|
||||
parent,
|
||||
|
|
@ -1247,7 +1212,7 @@ impl<T: Application> Cosmic<T> {
|
|||
core.applet.suggested_bounds = b;
|
||||
}
|
||||
Action::Opened(id) => {
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
if self.app.core().sync_window_border_radii_to_theme() {
|
||||
use iced_runtime::platform_specific::wayland::CornerRadius;
|
||||
use iced_winit::platform_specific::commands::corner_radius::corner_radius;
|
||||
|
|
@ -1296,14 +1261,14 @@ impl<App: Application> Cosmic<App> {
|
|||
pub fn new(app: App) -> Self {
|
||||
Self {
|
||||
app,
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
surface_views: HashMap::new(),
|
||||
tracked_windows: HashSet::new(),
|
||||
opened_surfaces: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
/// Create a subsurface
|
||||
pub fn get_subsurface(
|
||||
&mut self,
|
||||
|
|
@ -1326,7 +1291,7 @@ impl<App: Application> Cosmic<App> {
|
|||
get_subsurface(settings)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
/// Create a subsurface
|
||||
pub fn get_popup(
|
||||
&mut self,
|
||||
|
|
@ -1348,7 +1313,7 @@ impl<App: Application> Cosmic<App> {
|
|||
get_popup(settings)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
/// Create a window surface
|
||||
pub fn get_window(
|
||||
&mut self,
|
||||
|
|
|
|||
132
src/app/mod.rs
132
src/app/mod.rs
|
|
@ -11,8 +11,9 @@ pub use action::Action;
|
|||
use cosmic_config::CosmicConfigEntry;
|
||||
pub mod context_drawer;
|
||||
pub use context_drawer::{ContextDrawer, context_drawer};
|
||||
use iced::application::BootFn;
|
||||
pub mod cosmic;
|
||||
#[cfg(all(feature = "winit", feature = "multi-window"))]
|
||||
pub(crate) mod multi_window;
|
||||
pub mod settings;
|
||||
|
||||
pub type Task<M> = iced::Task<crate::Action<M>>;
|
||||
|
|
@ -20,13 +21,12 @@ pub type Task<M> = iced::Task<crate::Action<M>>;
|
|||
pub use crate::Core;
|
||||
use crate::prelude::*;
|
||||
use crate::theme::THEME;
|
||||
use crate::widget::{container, id_container, menu, nav_bar, popover, space};
|
||||
use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover};
|
||||
use apply::Apply;
|
||||
use iced::window;
|
||||
use iced::{Length, Subscription};
|
||||
use iced::{theme, window};
|
||||
pub use settings::Settings;
|
||||
use std::borrow::Cow;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
#[cold]
|
||||
pub(crate) fn iced_settings<App: Application>(
|
||||
|
|
@ -82,7 +82,7 @@ pub(crate) fn iced_settings<App: Application>(
|
|||
window_settings.min_size = Some(min_size);
|
||||
}
|
||||
let max_size = settings.size_limits.max();
|
||||
if max_size != iced::Size::INFINITE {
|
||||
if max_size != iced::Size::INFINITY {
|
||||
window_settings.max_size = Some(max_size);
|
||||
}
|
||||
|
||||
|
|
@ -90,99 +90,51 @@ pub(crate) fn iced_settings<App: Application>(
|
|||
(iced, (core, flags), window_settings)
|
||||
}
|
||||
|
||||
pub(crate) struct BootDataInner<A: crate::app::Application> {
|
||||
pub flags: A::Flags,
|
||||
pub core: Core,
|
||||
pub settings: window::Settings,
|
||||
}
|
||||
|
||||
pub(crate) struct BootData<A: crate::app::Application>(pub Rc<RefCell<Option<BootDataInner<A>>>>);
|
||||
|
||||
impl<A: crate::app::Application> BootFn<cosmic::Cosmic<A>, crate::Action<A::Message>>
|
||||
for BootData<A>
|
||||
{
|
||||
fn boot(&self) -> (cosmic::Cosmic<A>, iced::Task<crate::Action<A::Message>>) {
|
||||
let mut data = self.0.borrow_mut();
|
||||
let mut data = data.take().unwrap();
|
||||
let mut tasks = Vec::new();
|
||||
#[cfg(feature = "multi-window")]
|
||||
if data.core.main_window_id().is_some() {
|
||||
let window_task = iced_runtime::task::oneshot(|channel| {
|
||||
iced_runtime::Action::Window(iced_runtime::window::Action::Open(
|
||||
window::Id::RESERVED,
|
||||
data.settings,
|
||||
channel,
|
||||
))
|
||||
});
|
||||
data.core.set_main_window_id(Some(window::Id::RESERVED));
|
||||
tasks.push(window_task.discard());
|
||||
}
|
||||
let (a, t) = cosmic::Cosmic::<A>::init((data.core, data.flags));
|
||||
tasks.push(t);
|
||||
(a, Task::batch(tasks))
|
||||
}
|
||||
}
|
||||
/// Launch a COSMIC application with the given [`Settings`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error on application failure.
|
||||
pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result {
|
||||
#[cfg(feature = "desktop")]
|
||||
image_extras::register();
|
||||
|
||||
#[cfg(all(target_env = "gnu", not(target_os = "windows")))]
|
||||
if let Some(threshold) = settings.default_mmap_threshold {
|
||||
crate::malloc::limit_mmap_threshold(threshold);
|
||||
}
|
||||
|
||||
let default_font = settings.default_font;
|
||||
let (settings, (mut core, flags), window_settings) = iced_settings::<App>(settings, flags);
|
||||
let (settings, mut flags, window_settings) = iced_settings::<App>(settings, flags);
|
||||
#[cfg(not(feature = "multi-window"))]
|
||||
{
|
||||
core.main_window = Some(iced::window::Id::RESERVED);
|
||||
|
||||
flags.0.main_window = Some(iced::window::Id::RESERVED);
|
||||
iced::application(
|
||||
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
|
||||
flags,
|
||||
core,
|
||||
settings: window_settings.clone(),
|
||||
})))),
|
||||
cosmic::Cosmic::title,
|
||||
cosmic::Cosmic::update,
|
||||
cosmic::Cosmic::view,
|
||||
)
|
||||
.subscription(cosmic::Cosmic::subscription)
|
||||
.title(cosmic::Cosmic::title)
|
||||
.style(cosmic::Cosmic::style)
|
||||
.theme(cosmic::Cosmic::theme)
|
||||
.window_size((500.0, 800.0))
|
||||
.settings(settings)
|
||||
.window(window_settings)
|
||||
.run()
|
||||
.run_with(move || cosmic::Cosmic::<App>::init(flags))
|
||||
}
|
||||
#[cfg(feature = "multi-window")]
|
||||
{
|
||||
let no_main_window = core.main_window.is_none();
|
||||
if no_main_window {
|
||||
// app = app.window(window_settings);
|
||||
core.main_window = Some(iced_core::window::Id::RESERVED);
|
||||
}
|
||||
let app = iced::daemon(
|
||||
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
|
||||
flags,
|
||||
core,
|
||||
settings: window_settings,
|
||||
})))),
|
||||
let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>(
|
||||
cosmic::Cosmic::title,
|
||||
cosmic::Cosmic::update,
|
||||
cosmic::Cosmic::view,
|
||||
);
|
||||
|
||||
if flags.0.main_window.is_none() {
|
||||
app = app.window(window_settings);
|
||||
flags.0.main_window = Some(iced_core::window::Id::RESERVED);
|
||||
}
|
||||
app.subscription(cosmic::Cosmic::subscription)
|
||||
.title(cosmic::Cosmic::title)
|
||||
.style(cosmic::Cosmic::style)
|
||||
.theme(cosmic::Cosmic::theme)
|
||||
.settings(settings)
|
||||
.run()
|
||||
.run_with(move || cosmic::Cosmic::<App>::init(flags))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,9 +149,6 @@ where
|
|||
App::Flags: CosmicFlags,
|
||||
App::Message: Clone + std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
#[cfg(feature = "desktop")]
|
||||
image_extras::register();
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();
|
||||
|
|
@ -255,17 +204,13 @@ where
|
|||
tracing::info!("Another instance is running");
|
||||
Ok(())
|
||||
} else {
|
||||
let (settings, (mut core, flags), window_settings) = iced_settings::<App>(settings, flags);
|
||||
core.single_instance = true;
|
||||
let (settings, mut flags, window_settings) = iced_settings::<App>(settings, flags);
|
||||
flags.0.single_instance = true;
|
||||
|
||||
#[cfg(not(feature = "multi-window"))]
|
||||
{
|
||||
iced::application(
|
||||
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
|
||||
flags,
|
||||
core,
|
||||
settings: window_settings.clone(),
|
||||
})))),
|
||||
cosmic::Cosmic::title,
|
||||
cosmic::Cosmic::update,
|
||||
cosmic::Cosmic::view,
|
||||
)
|
||||
|
|
@ -275,31 +220,24 @@ where
|
|||
.window_size((500.0, 800.0))
|
||||
.settings(settings)
|
||||
.window(window_settings)
|
||||
.run()
|
||||
.run_with(move || cosmic::Cosmic::<App>::init(flags))
|
||||
}
|
||||
#[cfg(feature = "multi-window")]
|
||||
{
|
||||
let no_main_window = core.main_window.is_none();
|
||||
if no_main_window {
|
||||
// app = app.window(window_settings);
|
||||
core.main_window = Some(iced_core::window::Id::RESERVED);
|
||||
}
|
||||
let mut app = iced::daemon(
|
||||
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
|
||||
flags,
|
||||
core,
|
||||
settings: window_settings,
|
||||
})))),
|
||||
let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>(
|
||||
cosmic::Cosmic::title,
|
||||
cosmic::Cosmic::update,
|
||||
cosmic::Cosmic::view,
|
||||
);
|
||||
|
||||
if flags.0.main_window.is_none() {
|
||||
app = app.window(window_settings);
|
||||
flags.0.main_window = Some(iced_core::window::Id::RESERVED);
|
||||
}
|
||||
app.subscription(cosmic::Cosmic::subscription)
|
||||
.style(cosmic::Cosmic::style)
|
||||
.title(cosmic::Cosmic::title)
|
||||
.theme(cosmic::Cosmic::theme)
|
||||
.settings(settings)
|
||||
.run()
|
||||
.run_with(move || cosmic::Cosmic::<App>::init(flags))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -391,8 +329,9 @@ where
|
|||
.on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id)))
|
||||
.context_menu(self.nav_context_menu(self.core().nav_bar_context()))
|
||||
.into_container()
|
||||
// XXX both must be shrink to avoid flex layout from ignoring it
|
||||
.width(iced::Length::Shrink)
|
||||
.height(iced::Length::Fill);
|
||||
.height(iced::Length::Shrink);
|
||||
|
||||
if !self.core().is_condensed() {
|
||||
nav = nav.max_width(280);
|
||||
|
|
@ -489,7 +428,7 @@ where
|
|||
}
|
||||
|
||||
/// Overrides the default style for applications
|
||||
fn style(&self) -> Option<theme::Style> {
|
||||
fn style(&self) -> Option<iced_runtime::Appearance> {
|
||||
None
|
||||
}
|
||||
|
||||
|
|
@ -725,17 +664,16 @@ impl<App: Application> ApplicationExt for App {
|
|||
[0, 0, 0, 0]
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
)
|
||||
} else {
|
||||
//TODO: this element is added to workaround state issues
|
||||
widgets.push(space::horizontal().width(Length::Shrink).into());
|
||||
widgets.push(horizontal_space().width(Length::Shrink).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widgets
|
||||
});
|
||||
|
||||
let content_col = crate::widget::column::with_capacity(2)
|
||||
.push(content_row)
|
||||
.push_maybe(self.footer().map(|footer| {
|
||||
|
|
@ -748,8 +686,10 @@ impl<App: Application> ApplicationExt for App {
|
|||
}));
|
||||
let content: Element<_> = if content_container {
|
||||
content_col
|
||||
.apply(container)
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill)
|
||||
.class(crate::theme::Container::WindowBackground)
|
||||
.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
|
||||
.into()
|
||||
} else {
|
||||
|
|
@ -773,11 +713,11 @@ impl<App: Application> ApplicationExt for App {
|
|||
.focused(focused)
|
||||
.maximized(maximized)
|
||||
.sharp_corners(sharp_corners)
|
||||
.transparent(content_container)
|
||||
.title(&core.window.header_title)
|
||||
.on_drag(crate::Action::Cosmic(Action::Drag))
|
||||
.on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu))
|
||||
.on_double_click(crate::Action::Cosmic(Action::Maximize));
|
||||
.on_double_click(crate::Action::Cosmic(Action::Maximize))
|
||||
.is_condensed(is_condensed);
|
||||
|
||||
if self.nav_model().is_some() {
|
||||
let toggle = crate::widget::nav_bar_toggle()
|
||||
|
|
|
|||
244
src/app/multi_window.rs
Normal file
244
src/app/multi_window.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Create and run daemons that run in the background.
|
||||
//! Copied from iced 0.13, but adds optional initial window
|
||||
|
||||
use iced::application;
|
||||
use iced::window;
|
||||
use iced::{
|
||||
self, Program,
|
||||
program::{self, with_style, with_subscription, with_theme, with_title},
|
||||
runtime::{Appearance, DefaultStyle},
|
||||
};
|
||||
use iced::{Element, Result, Settings, Subscription, Task};
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub(crate) struct Instance<State, Message, Theme, Renderer, Update, View, Executor> {
|
||||
update: Update,
|
||||
view: View,
|
||||
_state: PhantomData<State>,
|
||||
_message: PhantomData<Message>,
|
||||
_theme: PhantomData<Theme>,
|
||||
_renderer: PhantomData<Renderer>,
|
||||
_executor: PhantomData<Executor>,
|
||||
}
|
||||
|
||||
/// Creates an iced [`MultiWindow`] given its title, update, and view logic.
|
||||
pub fn multi_window<State, Message, Theme, Renderer, Executor>(
|
||||
title: impl Title<State>,
|
||||
update: impl application::Update<State, Message>,
|
||||
view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>,
|
||||
) -> MultiWindow<impl Program<State = State, Message = Message, Theme = Theme>>
|
||||
where
|
||||
State: 'static,
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Theme: Default + DefaultStyle,
|
||||
Renderer: program::Renderer,
|
||||
Executor: iced::Executor,
|
||||
{
|
||||
use std::marker::PhantomData;
|
||||
|
||||
impl<State, Message, Theme, Renderer, Update, View, Executor> Program
|
||||
for Instance<State, Message, Theme, Renderer, Update, View, Executor>
|
||||
where
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Theme: Default + DefaultStyle,
|
||||
Renderer: program::Renderer,
|
||||
Update: application::Update<State, Message>,
|
||||
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
|
||||
Executor: iced::Executor,
|
||||
{
|
||||
type State = State;
|
||||
type Message = Message;
|
||||
type Theme = Theme;
|
||||
type Renderer = Renderer;
|
||||
type Executor = Executor;
|
||||
|
||||
fn update(&self, state: &mut Self::State, message: Self::Message) -> Task<Self::Message> {
|
||||
self.update.update(state, message).into()
|
||||
}
|
||||
|
||||
fn view<'a>(
|
||||
&self,
|
||||
state: &'a Self::State,
|
||||
window: window::Id,
|
||||
) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
|
||||
self.view.view(state, window).into()
|
||||
}
|
||||
}
|
||||
|
||||
MultiWindow {
|
||||
raw: Instance {
|
||||
update,
|
||||
view,
|
||||
_state: PhantomData,
|
||||
_message: PhantomData,
|
||||
_theme: PhantomData,
|
||||
_renderer: PhantomData,
|
||||
_executor: PhantomData::<Executor>,
|
||||
},
|
||||
settings: Settings::default(),
|
||||
window: None,
|
||||
}
|
||||
.title(title)
|
||||
}
|
||||
|
||||
/// The underlying definition and configuration of an iced daemon.
|
||||
///
|
||||
/// You can use this API to create and run iced applications
|
||||
/// step by step—without coupling your logic to a trait
|
||||
/// or a specific type.
|
||||
///
|
||||
/// You can create a [`MultiWindow`] with the [`daemon`] helper.
|
||||
#[derive(Debug)]
|
||||
pub struct MultiWindow<P: Program> {
|
||||
raw: P,
|
||||
settings: Settings,
|
||||
window: Option<window::Settings>,
|
||||
}
|
||||
|
||||
impl<P: Program> MultiWindow<P> {
|
||||
#[cfg(any(feature = "winit", feature = "wayland"))]
|
||||
/// Runs the [`MultiWindow`].
|
||||
///
|
||||
/// The state of the [`MultiWindow`] must implement [`Default`].
|
||||
/// If your state does not implement [`Default`], use [`run_with`]
|
||||
/// instead.
|
||||
///
|
||||
/// [`run_with`]: Self::run_with
|
||||
pub fn run(self) -> Result
|
||||
where
|
||||
Self: 'static,
|
||||
P::State: Default,
|
||||
{
|
||||
self.raw.run(self.settings, self.window)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "winit", feature = "wayland"))]
|
||||
/// Runs the [`MultiWindow`] with a closure that creates the initial state.
|
||||
pub fn run_with<I>(self, initialize: I) -> Result
|
||||
where
|
||||
Self: 'static,
|
||||
I: FnOnce() -> (P::State, Task<P::Message>) + 'static,
|
||||
{
|
||||
self.raw.run_with(self.settings, self.window, initialize)
|
||||
}
|
||||
|
||||
/// Sets the [`Settings`] that will be used to run the [`MultiWindow`].
|
||||
pub fn settings(self, settings: Settings) -> Self {
|
||||
Self { settings, ..self }
|
||||
}
|
||||
|
||||
/// Sets the [`Title`] of the [`MultiWindow`].
|
||||
pub(crate) fn title(
|
||||
self,
|
||||
title: impl Title<P::State>,
|
||||
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
|
||||
MultiWindow {
|
||||
raw: with_title(self.raw, move |state, window| title.title(state, window)),
|
||||
settings: self.settings,
|
||||
window: self.window,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the subscription logic of the [`MultiWindow`].
|
||||
pub fn subscription(
|
||||
self,
|
||||
f: impl Fn(&P::State) -> Subscription<P::Message>,
|
||||
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
|
||||
MultiWindow {
|
||||
raw: with_subscription(self.raw, f),
|
||||
settings: self.settings,
|
||||
window: self.window,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the theme logic of the [`MultiWindow`].
|
||||
pub fn theme(
|
||||
self,
|
||||
f: impl Fn(&P::State, window::Id) -> P::Theme,
|
||||
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
|
||||
MultiWindow {
|
||||
raw: with_theme(self.raw, f),
|
||||
settings: self.settings,
|
||||
window: self.window,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style logic of the [`MultiWindow`].
|
||||
pub fn style(
|
||||
self,
|
||||
f: impl Fn(&P::State, &P::Theme) -> Appearance,
|
||||
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
|
||||
MultiWindow {
|
||||
raw: with_style(self.raw, f),
|
||||
settings: self.settings,
|
||||
window: self.window,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the window settings of the [`MultiWindow`].
|
||||
pub fn window(self, window: window::Settings) -> Self {
|
||||
Self {
|
||||
raw: self.raw,
|
||||
settings: self.settings,
|
||||
window: Some(window),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The title logic of some [`MultiWindow`].
|
||||
///
|
||||
/// This trait is implemented both for `&static str` and
|
||||
/// any closure `Fn(&State, window::Id) -> String`.
|
||||
///
|
||||
/// This trait allows the [`daemon`] builder to take any of them.
|
||||
pub trait Title<State> {
|
||||
/// Produces the title of the [`MultiWindow`].
|
||||
fn title(&self, state: &State, window: window::Id) -> String;
|
||||
}
|
||||
|
||||
impl<State> Title<State> for &'static str {
|
||||
fn title(&self, _state: &State, _window: window::Id) -> String {
|
||||
(*self).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, State> Title<State> for T
|
||||
where
|
||||
T: Fn(&State, window::Id) -> String,
|
||||
{
|
||||
fn title(&self, state: &State, window: window::Id) -> String {
|
||||
self(state, window)
|
||||
}
|
||||
}
|
||||
|
||||
/// The view logic of some [`MultiWindow`].
|
||||
///
|
||||
/// This trait allows the [`daemon`] builder to take any closure that
|
||||
/// returns any `Into<Element<'_, Message>>`.
|
||||
pub trait View<'a, State, Message, Theme, Renderer> {
|
||||
/// Produces the widget of the [`MultiWindow`].
|
||||
fn view(
|
||||
&self,
|
||||
state: &'a State,
|
||||
window: window::Id,
|
||||
) -> impl Into<Element<'a, Message, Theme, Renderer>>;
|
||||
}
|
||||
|
||||
impl<'a, T, State, Message, Theme, Renderer, Widget> View<'a, State, Message, Theme, Renderer> for T
|
||||
where
|
||||
T: Fn(&'a State, window::Id) -> Widget,
|
||||
State: 'static,
|
||||
Widget: Into<Element<'a, Message, Theme, Renderer>>,
|
||||
{
|
||||
fn view(
|
||||
&self,
|
||||
state: &'a State,
|
||||
window: window::Id,
|
||||
) -> impl Into<Element<'a, Message, Theme, Renderer>> {
|
||||
self(state, window)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ pub struct Settings {
|
|||
pub(crate) antialiasing: bool,
|
||||
|
||||
/// Autosize the window to fit its contents
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
pub(crate) autosize: bool,
|
||||
|
||||
/// Set the application to not create a main window
|
||||
|
|
@ -80,7 +80,7 @@ impl Default for Settings {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
antialiasing: true,
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
autosize: false,
|
||||
no_main_window: false,
|
||||
client_decorations: true,
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ where
|
|||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
|
|
@ -233,26 +233,25 @@ where
|
|||
self.padding,
|
||||
self.spacing,
|
||||
self.align,
|
||||
&mut self.children,
|
||||
&self.children,
|
||||
&mut tree.children,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds());
|
||||
operation.traverse(&mut |operation| {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|((child, state), c_layout)| {
|
||||
child.as_widget_mut().operate(
|
||||
child.as_widget().operate(
|
||||
state,
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
renderer,
|
||||
|
|
@ -262,17 +261,17 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn update(
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
) -> event::Status {
|
||||
let my_state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if let Some(hovered) = my_state.hovered {
|
||||
|
|
@ -286,7 +285,7 @@ where
|
|||
e,
|
||||
mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. }
|
||||
) {
|
||||
return self.children[hovered].as_widget_mut().update(
|
||||
return self.children[hovered].as_widget_mut().on_event(
|
||||
&mut tree.children[hovered],
|
||||
event,
|
||||
child_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
|
|
@ -303,7 +302,7 @@ where
|
|||
iced::core::touch::Event::FingerLifted { .. }
|
||||
| iced::core::touch::Event::FingerLost { .. }
|
||||
) {
|
||||
return self.children[hovered].as_widget_mut().update(
|
||||
return self.children[hovered].as_widget_mut().on_event(
|
||||
&mut tree.children[hovered],
|
||||
event,
|
||||
child_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
|
|
@ -320,49 +319,49 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
for (((i, child), state), c_layout) in self
|
||||
.children
|
||||
self.children
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
let mut cursor_virtual = cursor;
|
||||
if matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
|
||||
| Event::Touch(
|
||||
iced_core::touch::Event::FingerMoved { .. }
|
||||
| iced_core::touch::Event::FingerPressed { .. }
|
||||
)
|
||||
) && cursor.is_over(c_layout.bounds())
|
||||
{
|
||||
my_state.hovered = Some(i);
|
||||
return child.as_widget_mut().update(
|
||||
.map(|(((i, child), state), c_layout)| {
|
||||
let mut cursor_virtual = cursor;
|
||||
if matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
|
||||
| Event::Touch(
|
||||
iced_core::touch::Event::FingerMoved { .. }
|
||||
| iced_core::touch::Event::FingerPressed { .. }
|
||||
)
|
||||
) && cursor.is_over(c_layout.bounds())
|
||||
{
|
||||
my_state.hovered = Some(i);
|
||||
return child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor_virtual,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
} else if my_state.hovered.is_some_and(|h| i != h) {
|
||||
cursor_virtual = mouse::Cursor::Unavailable;
|
||||
}
|
||||
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
&event,
|
||||
event.clone(),
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor_virtual,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
} else if my_state.hovered.is_some_and(|h| i != h) {
|
||||
cursor_virtual = mouse::Cursor::Unavailable;
|
||||
}
|
||||
|
||||
child.as_widget_mut().update(
|
||||
state,
|
||||
&event,
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor_virtual,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -437,19 +436,11 @@ where
|
|||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'b>,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
overlay::from_children(
|
||||
&mut self.children,
|
||||
tree,
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
overlay::from_children(&mut self.children, tree, layout, renderer, translation)
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
|
|
|
|||
|
|
@ -1,36 +1,34 @@
|
|||
#[cfg(feature = "applet-token")]
|
||||
pub mod token;
|
||||
|
||||
use crate::app::{BootData, BootDataInner, cosmic};
|
||||
use crate::app::cosmic;
|
||||
use crate::{
|
||||
Application, Element, Renderer,
|
||||
app::iced_settings,
|
||||
cctk::sctk,
|
||||
iced::{
|
||||
self, Color, Length, Limits, Rectangle,
|
||||
alignment::{Alignment, Horizontal, Vertical},
|
||||
widget::Container,
|
||||
window,
|
||||
},
|
||||
iced_widget,
|
||||
theme::{self, Button, THEME, system_dark, system_light},
|
||||
widget::{
|
||||
self,
|
||||
autosize::{self, Autosize, autosize},
|
||||
column::Column,
|
||||
layer_container,
|
||||
horizontal_space, layer_container,
|
||||
row::Row,
|
||||
space::horizontal,
|
||||
space::vertical,
|
||||
vertical_space,
|
||||
},
|
||||
};
|
||||
|
||||
pub use cosmic_panel_config;
|
||||
use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
|
||||
use iced::{
|
||||
self, Color, Length, Limits, Rectangle,
|
||||
alignment::{Alignment, Horizontal, Vertical},
|
||||
widget::Container,
|
||||
window,
|
||||
};
|
||||
use iced_core::{Padding, Shadow};
|
||||
use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
|
||||
use iced_widget::Text;
|
||||
use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
|
||||
use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
|
||||
use std::cell::RefCell;
|
||||
use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration};
|
||||
use tracing::info;
|
||||
|
||||
|
|
@ -42,7 +40,7 @@ static AUTOSIZE_ID: LazyLock<iced::id::Id> =
|
|||
static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> =
|
||||
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main"));
|
||||
static TOOLTIP_ID: LazyLock<crate::widget::Id> = LazyLock::new(|| iced::id::Id::new("subsurface"));
|
||||
pub(crate) static TOOLTIP_WINDOW_ID: LazyLock<window::Id> = LazyLock::new(window::Id::unique);
|
||||
static TOOLTIP_WINDOW_ID: LazyLock<window::Id> = LazyLock::new(window::Id::unique);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
|
|
@ -226,7 +224,7 @@ impl Context {
|
|||
let symbolic = icon.symbolic;
|
||||
let icon = widget::icon(icon)
|
||||
.class(if symbolic {
|
||||
theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style {
|
||||
theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style {
|
||||
color: Some(theme.cosmic().background.on.into()),
|
||||
}))
|
||||
} else {
|
||||
|
|
@ -388,10 +386,10 @@ impl Context {
|
|||
},
|
||||
shadow: Shadow::default(),
|
||||
icon_color: Some(cosmic.background.on.into()),
|
||||
snap: true,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink)
|
||||
.align_x(horizontal_align)
|
||||
.align_y(vertical_align),
|
||||
|
|
@ -573,33 +571,26 @@ pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
|
|||
|
||||
// TODO make multi-window not mandatory
|
||||
|
||||
let no_main_window = core.main_window.is_none();
|
||||
if no_main_window {
|
||||
// TODO still apply window settings?
|
||||
// window_settings = window_settings.clone();
|
||||
core.main_window = Some(iced_core::window::Id::RESERVED);
|
||||
}
|
||||
let mut app = iced::daemon(
|
||||
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
|
||||
flags,
|
||||
core,
|
||||
settings: window_settings,
|
||||
})))),
|
||||
let mut app = super::app::multi_window::multi_window::<_, _, _, _, App::Executor>(
|
||||
cosmic::Cosmic::title,
|
||||
cosmic::Cosmic::update,
|
||||
cosmic::Cosmic::view,
|
||||
);
|
||||
|
||||
if core.main_window.is_none() {
|
||||
app = app.window(window_settings.clone());
|
||||
core.main_window = Some(iced_core::window::Id::RESERVED);
|
||||
}
|
||||
app.subscription(cosmic::Cosmic::subscription)
|
||||
.style(cosmic::Cosmic::style)
|
||||
.theme(cosmic::Cosmic::theme)
|
||||
.settings(iced_settings)
|
||||
.run()
|
||||
.run_with(move || cosmic::Cosmic::<App>::init((core, flags)))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn style() -> iced::theme::Style {
|
||||
pub fn style() -> iced_runtime::Appearance {
|
||||
let theme = crate::theme::THEME.lock().unwrap();
|
||||
iced::theme::Style {
|
||||
iced_runtime::Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
icon_color: theme.cosmic().on_bg_color().into(),
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ where
|
|||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
|
|
@ -222,26 +222,25 @@ where
|
|||
self.padding,
|
||||
self.spacing,
|
||||
self.align,
|
||||
&mut self.children,
|
||||
&self.children,
|
||||
&mut tree.children,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds());
|
||||
operation.traverse(&mut |operation| {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|((child, state), c_layout)| {
|
||||
child.as_widget_mut().operate(
|
||||
child.as_widget().operate(
|
||||
state,
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
renderer,
|
||||
|
|
@ -251,17 +250,17 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn update(
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
) -> event::Status {
|
||||
let my_state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if let Some(hovered) = my_state.hovered {
|
||||
|
|
@ -275,7 +274,7 @@ where
|
|||
e,
|
||||
mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. }
|
||||
) {
|
||||
return self.children[hovered].as_widget_mut().update(
|
||||
return self.children[hovered].as_widget_mut().on_event(
|
||||
&mut tree.children[hovered],
|
||||
event,
|
||||
child_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
|
|
@ -292,7 +291,7 @@ where
|
|||
iced::core::touch::Event::FingerLifted { .. }
|
||||
| iced::core::touch::Event::FingerLost { .. }
|
||||
) {
|
||||
return self.children[hovered].as_widget_mut().update(
|
||||
return self.children[hovered].as_widget_mut().on_event(
|
||||
&mut tree.children[hovered],
|
||||
event,
|
||||
child_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
|
|
@ -309,50 +308,50 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
for (((i, child), state), c_layout) in self
|
||||
.children
|
||||
self.children
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
let mut cursor_virtual = cursor;
|
||||
.map(|(((i, child), state), c_layout)| {
|
||||
let mut cursor_virtual = cursor;
|
||||
|
||||
if matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
|
||||
| Event::Touch(
|
||||
iced_core::touch::Event::FingerMoved { .. }
|
||||
| iced_core::touch::Event::FingerPressed { .. }
|
||||
)
|
||||
) && cursor.is_over(c_layout.bounds())
|
||||
{
|
||||
my_state.hovered = Some(i);
|
||||
return child.as_widget_mut().update(
|
||||
if matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
|
||||
| Event::Touch(
|
||||
iced_core::touch::Event::FingerMoved { .. }
|
||||
| iced_core::touch::Event::FingerPressed { .. }
|
||||
)
|
||||
) && cursor.is_over(c_layout.bounds())
|
||||
{
|
||||
my_state.hovered = Some(i);
|
||||
return child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor_virtual,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
} else if my_state.hovered.is_some_and(|h| i != h) {
|
||||
cursor_virtual = mouse::Cursor::Unavailable;
|
||||
}
|
||||
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
&event,
|
||||
event.clone(),
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor_virtual,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
} else if my_state.hovered.is_some_and(|h| i != h) {
|
||||
cursor_virtual = mouse::Cursor::Unavailable;
|
||||
}
|
||||
|
||||
child.as_widget_mut().update(
|
||||
state,
|
||||
&event,
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor_virtual,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -427,19 +426,11 @@ where
|
|||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'b>,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
overlay::from_children(
|
||||
&mut self.children,
|
||||
tree,
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
overlay::from_children(&mut self.children, tree, layout, renderer, translation)
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use crate::iced;
|
||||
use crate::iced_futures::futures;
|
||||
use cctk::sctk::reexports::calloop;
|
||||
use futures::{
|
||||
SinkExt, StreamExt,
|
||||
channel::mpsc::{UnboundedReceiver, unbounded},
|
||||
};
|
||||
use iced::Subscription;
|
||||
use iced_futures::futures;
|
||||
use iced_futures::stream;
|
||||
use std::{fmt::Debug, hash::Hash, thread::JoinHandle};
|
||||
|
||||
|
|
@ -14,15 +14,16 @@ use super::wayland_handler::wayland_handler;
|
|||
pub fn activation_token_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
) -> iced::Subscription<TokenUpdate> {
|
||||
Subscription::run_with(id, |_| {
|
||||
Subscription::run_with_id(
|
||||
id,
|
||||
stream::channel(50, move |mut output| async move {
|
||||
let mut state = State::Ready;
|
||||
|
||||
loop {
|
||||
state = start_listening(state, &mut output).await;
|
||||
}
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
use std::os::fd::AsFd;
|
||||
|
||||
use iced::window;
|
||||
|
||||
/// Initiates a window drag.
|
||||
|
|
@ -39,35 +36,10 @@ pub fn set_theme<M: Send + 'static>(theme: crate::Theme) -> iced::Task<crate::Ac
|
|||
|
||||
/// Sets the window mode to windowed.
|
||||
pub fn set_windowed<M>(id: window::Id) -> iced::Task<crate::Action<M>> {
|
||||
iced_runtime::window::set_mode(id, window::Mode::Windowed)
|
||||
iced_runtime::window::change_mode(id, window::Mode::Windowed)
|
||||
}
|
||||
|
||||
/// Toggles the windows' maximize state.
|
||||
pub fn toggle_maximize<M>(id: window::Id) -> iced::Task<crate::Action<M>> {
|
||||
iced_runtime::window::toggle_maximize(id)
|
||||
}
|
||||
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
pub fn file_transfer_send(
|
||||
writeable: bool,
|
||||
auto_stop: bool,
|
||||
files: Vec<impl AsFd + Send + Sync + 'static>,
|
||||
) -> iced::Task<ashpd::Result<String>> {
|
||||
iced::Task::future(async move {
|
||||
let file_transfer = ashpd::documents::FileTransfer::new().await?;
|
||||
let key = file_transfer.start_transfer(writeable, auto_stop).await?;
|
||||
file_transfer.add_files(&key, &files).await?;
|
||||
Ok(key)
|
||||
})
|
||||
}
|
||||
|
||||
/// Receive the files offered over the xdg share portal using the `key`.
|
||||
/// Returns a list of file paths.
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
pub fn file_transfer_receive(key: String) -> iced::Task<ashpd::Result<Vec<String>>> {
|
||||
dbg!(&key);
|
||||
iced::Task::future(async move {
|
||||
let file_transfer = ashpd::documents::FileTransfer::new().await?;
|
||||
file_transfer.retrieve_files(&key).await
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
|||
use cosmic_config::{Config, CosmicConfigEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
use std::sync::{LazyLock, Mutex, RwLock};
|
||||
|
||||
/// ID for the `CosmicTk` config.
|
||||
pub const ID: &str = "com.system76.CosmicTk";
|
||||
|
|
@ -16,6 +16,9 @@ pub const ID: &str = "com.system76.CosmicTk";
|
|||
const MONO_FAMILY_DEFAULT: &str = "Noto Sans Mono";
|
||||
const SANS_FAMILY_DEFAULT: &str = "Open Sans";
|
||||
|
||||
/// Stores static strings of the family names for `iced::Font` compatibility.
|
||||
pub static FAMILY_MAP: LazyLock<Mutex<BTreeSet<&'static str>>> = LazyLock::new(Mutex::default);
|
||||
|
||||
pub static COSMIC_TK: LazyLock<RwLock<CosmicTk>> = LazyLock::new(|| {
|
||||
RwLock::new(
|
||||
CosmicTk::config()
|
||||
|
|
@ -153,19 +156,16 @@ pub struct FontConfig {
|
|||
|
||||
impl From<FontConfig> for iced::Font {
|
||||
fn from(font: FontConfig) -> Self {
|
||||
/// Stores static strings of the family names for `iced::Font` compatibility.
|
||||
static FAMILY_MAP: LazyLock<RwLock<BTreeSet<&'static str>>> =
|
||||
LazyLock::new(RwLock::default);
|
||||
let mut family_map = FAMILY_MAP.lock().unwrap();
|
||||
|
||||
let read_guard = FAMILY_MAP.read().unwrap();
|
||||
let name: Option<&'static str> = read_guard.get(font.family.as_str()).copied();
|
||||
drop(read_guard);
|
||||
|
||||
let name = name.unwrap_or_else(|| {
|
||||
let value: &'static str = font.family.clone().leak();
|
||||
FAMILY_MAP.write().unwrap().insert(value);
|
||||
value
|
||||
});
|
||||
let name: &'static str = family_map
|
||||
.get(font.family.as_str())
|
||||
.copied()
|
||||
.unwrap_or_else(|| {
|
||||
let value = font.family.clone().leak();
|
||||
family_map.insert(value);
|
||||
value
|
||||
});
|
||||
|
||||
Self {
|
||||
family: iced::font::Family::Name(name),
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ pub struct Core {
|
|||
|
||||
pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
pub(crate) sync_window_border_radii_to_theme: bool,
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ impl Default for Core {
|
|||
main_window: None,
|
||||
exit_on_main_window_closed: true,
|
||||
menu_bars: HashMap::new(),
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
sync_window_border_radii_to_theme: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -493,12 +493,12 @@ impl Core {
|
|||
}
|
||||
|
||||
// TODO should we emit tasks setting the corner radius or unsetting it if this is changed?
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) {
|
||||
self.sync_window_border_radii_to_theme = sync;
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(feature = "wayland")]
|
||||
pub fn sync_window_border_radii_to_theme(&self) -> bool {
|
||||
self.sync_window_border_radii_to_theme
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,80 +16,75 @@ use {
|
|||
#[cold]
|
||||
pub fn subscription<App: ApplicationExt>() -> Subscription<crate::Action<App::Message>> {
|
||||
use iced_futures::futures::StreamExt;
|
||||
iced_futures::Subscription::run_with(TypeId::of::<DbusActivation>(), |_| {
|
||||
iced::stream::channel(
|
||||
10,
|
||||
move |mut output: Sender<crate::Action<App::Message>>| async move {
|
||||
let mut single_instance: DbusActivation = DbusActivation::new();
|
||||
let mut rx = single_instance.rx();
|
||||
if let Ok(builder) = zbus::connection::Builder::session() {
|
||||
let path: String = format!("/{}", App::APP_ID.replace('.', "/"));
|
||||
if let Ok(conn) = builder.build().await {
|
||||
// XXX Setup done this way seems to be more reliable.
|
||||
//
|
||||
// the docs for serve_at seem to imply it will replace the
|
||||
// existing interface at the requested path, but it doesn't
|
||||
// seem to work that way all the time. The docs for
|
||||
// object_server().at() imply it won't replace the existing
|
||||
// interface.
|
||||
//
|
||||
// request_name is used either way, with the builder or
|
||||
// with the connection, but it must be done after the
|
||||
// object server is setup.
|
||||
if conn.object_server().at(path, single_instance).await != Ok(true) {
|
||||
tracing::error!("Failed to serve dbus");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if conn.request_name(App::APP_ID).await.is_err() {
|
||||
tracing::error!("Failed to serve dbus");
|
||||
std::process::exit(1);
|
||||
}
|
||||
iced_futures::Subscription::run_with_id(
|
||||
TypeId::of::<DbusActivation>(),
|
||||
iced::stream::channel(10, move |mut output| async move {
|
||||
let mut single_instance: DbusActivation = DbusActivation::new();
|
||||
let mut rx = single_instance.rx();
|
||||
if let Ok(builder) = zbus::connection::Builder::session() {
|
||||
let path: String = format!("/{}", App::APP_ID.replace('.', "/"));
|
||||
if let Ok(conn) = builder.build().await {
|
||||
// XXX Setup done this way seems to be more reliable.
|
||||
//
|
||||
// the docs for serve_at seem to imply it will replace the
|
||||
// existing interface at the requested path, but it doesn't
|
||||
// seem to work that way all the time. The docs for
|
||||
// object_server().at() imply it won't replace the existing
|
||||
// interface.
|
||||
//
|
||||
// request_name is used either way, with the builder or
|
||||
// with the connection, but it must be done after the
|
||||
// object server is setup.
|
||||
if conn.object_server().at(path, single_instance).await != Ok(true) {
|
||||
tracing::error!("Failed to serve dbus");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if conn.request_name(App::APP_ID).await.is_err() {
|
||||
tracing::error!("Failed to serve dbus");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
output
|
||||
.send(crate::Action::Cosmic(crate::app::Action::DbusConnection(
|
||||
conn.clone(),
|
||||
)))
|
||||
.await;
|
||||
output
|
||||
.send(crate::Action::Cosmic(crate::app::Action::DbusConnection(
|
||||
conn.clone(),
|
||||
)))
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "smol")]
|
||||
let handle = {
|
||||
std::thread::spawn(move || {
|
||||
let conn_clone = _conn.clone();
|
||||
#[cfg(feature = "smol")]
|
||||
let handle = {
|
||||
std::thread::spawn(move || {
|
||||
let conn_clone = _conn.clone();
|
||||
|
||||
zbus::block_on(async move {
|
||||
loop {
|
||||
conn_clone.executor().tick().await;
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
while let Some(mut msg) = rx.next().await {
|
||||
if let Some(token) = msg.activation_token.take() {
|
||||
if let Err(err) = output
|
||||
.send(crate::Action::Cosmic(crate::app::Action::Activate(
|
||||
token,
|
||||
)))
|
||||
.await
|
||||
{
|
||||
tracing::error!(?err, "Failed to send message");
|
||||
zbus::block_on(async move {
|
||||
loop {
|
||||
conn_clone.executor().tick().await;
|
||||
}
|
||||
}
|
||||
if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await
|
||||
})
|
||||
})
|
||||
};
|
||||
while let Some(mut msg) = rx.next().await {
|
||||
if let Some(token) = msg.activation_token.take() {
|
||||
if let Err(err) = output
|
||||
.send(crate::Action::Cosmic(crate::app::Action::Activate(token)))
|
||||
.await
|
||||
{
|
||||
tracing::error!(?err, "Failed to send message");
|
||||
}
|
||||
}
|
||||
if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await {
|
||||
tracing::error!(?err, "Failed to send message");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Failed to connect to dbus for single instance");
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Failed to connect to dbus for single instance");
|
||||
}
|
||||
|
||||
loop {
|
||||
iced::futures::pending!();
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
loop {
|
||||
iced::futures::pending!();
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
|||
|
|
@ -7,22 +7,23 @@ use std::path::{Path, PathBuf};
|
|||
use std::{borrow::Cow, collections::HashSet, ffi::OsStr};
|
||||
|
||||
pub trait IconSourceExt {
|
||||
fn as_cosmic_icon(&self) -> crate::widget::icon::Handle;
|
||||
fn as_cosmic_icon(&self) -> crate::widget::icon::Icon;
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
impl IconSourceExt for fde::IconSource {
|
||||
fn as_cosmic_icon(&self) -> crate::widget::icon::Handle {
|
||||
fn as_cosmic_icon(&self) -> crate::widget::icon::Icon {
|
||||
match self {
|
||||
fde::IconSource::Name(name) => crate::widget::icon::from_name(name.as_str())
|
||||
.prefer_svg(true)
|
||||
.size(128)
|
||||
.fallback(Some(crate::widget::icon::IconFallback::Names(vec![
|
||||
"application-default".into(),
|
||||
"application-x-executable".into(),
|
||||
])))
|
||||
.handle(),
|
||||
fde::IconSource::Path(path) => crate::widget::icon::from_path(path.clone()),
|
||||
.into(),
|
||||
fde::IconSource::Path(path) => {
|
||||
crate::widget::icon(crate::widget::icon::from_path(path.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -416,6 +417,7 @@ fn match_exec_basename(
|
|||
};
|
||||
|
||||
let basename_lower = basename.to_ascii_lowercase();
|
||||
|
||||
if normalized
|
||||
.iter()
|
||||
.any(|candidate| candidate == &basename_lower)
|
||||
|
|
@ -439,7 +441,8 @@ fn fallback_entry(context: &DesktopLookupContext<'_>) -> fde::DesktopEntry {
|
|||
let name = context
|
||||
.title
|
||||
.as_ref()
|
||||
.map_or_else(|| context.app_id.to_string(), |title| title.to_string());
|
||||
.map(|title| title.to_string())
|
||||
.unwrap_or_else(|| context.app_id.to_string());
|
||||
entry.add_desktop_entry("Name".to_string(), name);
|
||||
entry
|
||||
}
|
||||
|
|
@ -456,9 +459,7 @@ fn proton_or_wine_fallback(
|
|||
) -> Option<fde::DesktopEntry> {
|
||||
let app_id = context.app_id.as_ref();
|
||||
let is_proton_game = app_id == "steam_app_default";
|
||||
let is_wine_entry = std::path::Path::new(app_id)
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("exe"));
|
||||
let is_wine_entry = app_id.ends_with(".exe");
|
||||
|
||||
if !is_proton_game && !is_wine_entry {
|
||||
return None;
|
||||
|
|
@ -487,6 +488,10 @@ fn proton_or_wine_fallback(
|
|||
|
||||
#[cfg(not(windows))]
|
||||
fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec<String> {
|
||||
const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"];
|
||||
let mut ordered = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
fn push_candidate(seen: &mut HashSet<String>, ordered: &mut Vec<String>, candidate: &str) {
|
||||
let trimmed = candidate.trim();
|
||||
if trimmed.is_empty() {
|
||||
|
|
@ -527,11 +532,11 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec<String> {
|
|||
}
|
||||
}
|
||||
|
||||
if trimmed.contains('.')
|
||||
&& let Some(last) = trimmed.rsplit('.').next()
|
||||
{
|
||||
if last.len() >= 2 {
|
||||
push_candidate(seen, ordered, last);
|
||||
if trimmed.contains('.') {
|
||||
if let Some(last) = trimmed.rsplit('.').next() {
|
||||
if last.len() >= 2 {
|
||||
push_candidate(seen, ordered, last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -542,20 +547,13 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec<String> {
|
|||
push_candidate(seen, ordered, &trimmed.replace('_', "-"));
|
||||
}
|
||||
|
||||
for token in
|
||||
trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@') || c.is_whitespace())
|
||||
{
|
||||
for token in trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@' | ' ')) {
|
||||
if token.len() >= 2 && token != trimmed {
|
||||
push_candidate(seen, ordered, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"];
|
||||
|
||||
let mut ordered = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
add_variants(
|
||||
&mut seen,
|
||||
&mut ordered,
|
||||
|
|
@ -789,7 +787,7 @@ pub async fn spawn_desktop_exec<S, I, K, V>(
|
|||
})
|
||||
.unwrap_or_else(|| String::from("cosmic-term"));
|
||||
|
||||
term_exec = format!("{term} -e {}", exec.as_ref());
|
||||
term_exec = format!("{term} -- {}", exec.as_ref());
|
||||
&term_exec
|
||||
} else {
|
||||
exec.as_ref()
|
||||
|
|
@ -883,9 +881,7 @@ mod tests {
|
|||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &Path) -> Self {
|
||||
let original = env::var(key).ok();
|
||||
// std::env::{set_var, remove_var} are unsafe on newer toolchains;
|
||||
// we limit scope here to the test helper that toggles a single key.
|
||||
unsafe { std::env::set_var(key, value) };
|
||||
std::env::set_var(key, value);
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
|
@ -893,9 +889,9 @@ mod tests {
|
|||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(ref original) = self.original {
|
||||
unsafe { std::env::set_var(self.key, original) };
|
||||
std::env::set_var(self.key, original);
|
||||
} else {
|
||||
unsafe { std::env::remove_var(self.key) };
|
||||
std::env::remove_var(self.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -918,20 +914,12 @@ mod tests {
|
|||
let candidates = candidate_desktop_ids(&ctx);
|
||||
|
||||
assert_eq!(candidates.first().unwrap(), "com.example.App.desktop");
|
||||
for test in [
|
||||
"com.example.App",
|
||||
"com-example-App",
|
||||
"com_example_App",
|
||||
"Example App",
|
||||
"Example",
|
||||
"App",
|
||||
] {
|
||||
assert!(
|
||||
candidates
|
||||
.iter()
|
||||
.any(|c| c.to_ascii_lowercase() == test.to_ascii_lowercase()),
|
||||
);
|
||||
}
|
||||
assert!(candidates.contains(&"com.example.App".to_string()));
|
||||
assert!(candidates.contains(&"com-example-App".to_string()));
|
||||
assert!(candidates.contains(&"com_example_App".to_string()));
|
||||
assert!(candidates.contains(&"Example App".to_string()));
|
||||
assert!(candidates.contains(&"Example".to_string()));
|
||||
assert!(candidates.contains(&"App".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -996,7 +984,7 @@ Icon=vmware-workstation\n\
|
|||
|
||||
let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default());
|
||||
|
||||
assert_eq!(resolved.id(), "vmware-workstation");
|
||||
assert_eq!(resolved.id(), "vmware-workstation.desktop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1120,8 +1108,7 @@ Icon=vmware-workstation\n\
|
|||
let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default());
|
||||
assert!(resolved.icon().is_some());
|
||||
assert!(resolved.exec().is_some());
|
||||
let expected = format!("crx_{}", id);
|
||||
assert_eq!(resolved.startup_wm_class(), Some(expected.as_str()));
|
||||
assert_eq!(resolved.startup_wm_class(), Some(&format!("crx_{}", id)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue