Refine shell status icons and window controls; add installer
No files matched your search
@@ -2355,6 +2355,7 @@ dependencies = [
|
|||||||
"cosmic-client-toolkit",
|
"cosmic-client-toolkit",
|
||||||
"dnd",
|
"dnd",
|
||||||
"glam",
|
"glam",
|
||||||
|
"iced_accessibility",
|
||||||
"lilt",
|
"lilt",
|
||||||
"log",
|
"log",
|
||||||
"mime 0.1.0",
|
"mime 0.1.0",
|
||||||
@@ -2445,6 +2446,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"cosmic-client-toolkit",
|
"cosmic-client-toolkit",
|
||||||
"dnd",
|
"dnd",
|
||||||
|
"iced_accessibility",
|
||||||
"iced_core",
|
"iced_core",
|
||||||
"iced_futures",
|
"iced_futures",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
@@ -2509,6 +2511,7 @@ dependencies = [
|
|||||||
"build_helpers",
|
"build_helpers",
|
||||||
"cosmic-client-toolkit",
|
"cosmic-client-toolkit",
|
||||||
"dnd",
|
"dnd",
|
||||||
|
"iced_accessibility",
|
||||||
"iced_renderer",
|
"iced_renderer",
|
||||||
"iced_runtime",
|
"iced_runtime",
|
||||||
"log",
|
"log",
|
||||||
@@ -2529,6 +2532,7 @@ dependencies = [
|
|||||||
"cosmic-client-toolkit",
|
"cosmic-client-toolkit",
|
||||||
"cursor-icon",
|
"cursor-icon",
|
||||||
"dnd",
|
"dnd",
|
||||||
|
"iced_accessibility",
|
||||||
"iced_debug",
|
"iced_debug",
|
||||||
"iced_futures",
|
"iced_futures",
|
||||||
"iced_graphics",
|
"iced_graphics",
|
||||||
@@ -3030,6 +3034,7 @@ dependencies = [
|
|||||||
"i18n-embed",
|
"i18n-embed",
|
||||||
"i18n-embed-fl",
|
"i18n-embed-fl",
|
||||||
"iced",
|
"iced",
|
||||||
|
"iced_accessibility",
|
||||||
"iced_core",
|
"iced_core",
|
||||||
"iced_futures",
|
"iced_futures",
|
||||||
"iced_renderer",
|
"iced_renderer",
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ target/release/ferese --backend nested --grant-effects --grant-shell-control --
|
|||||||
```
|
```
|
||||||
|
|
||||||
The Ferese mark enters overview, where workspace indicators become available.
|
The Ferese mark enters overview, where workspace indicators become available.
|
||||||
The focused app appears on the left; status controls and day/time sit on the
|
The active workspace's window list appears on the left, with click-to-focus
|
||||||
|
and a highlighted active window; status controls and day/time sit on the
|
||||||
right. The 28px bar has square screen edges, 19px icons, and no border or drop
|
right. The 28px bar has square screen edges, 19px icons, and no border or drop
|
||||||
shadow. Window spacing uses the workspace outer gap; `top_bar_window_gap` under
|
shadow. Window spacing uses the workspace outer gap; `top_bar_window_gap` under
|
||||||
`[theme.geometry]` adds optional extra space (default 0). Solid and translucent
|
`[theme.geometry]` adds optional extra space (default 0). Solid and translucent
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
# Fedora / SDDM installation
|
# Fedora / SDDM installation
|
||||||
|
|
||||||
Build and test in the checkout:
|
From the checkout, run the installer as your normal user:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It builds all three release binaries with the locked dependencies, then uses
|
||||||
|
`sudo` (or `pkexec`) for installation. Install the Rust toolchain, native build
|
||||||
|
dependencies, `desktop-file-utils`, and D-Bus first. Cargo reports missing native
|
||||||
|
libraries during the build. The shell requires Rust 1.93 or newer.
|
||||||
|
|
||||||
|
Useful options:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/install.sh --dry-run
|
||||||
|
./scripts/install.sh --offline --release-id my-demo
|
||||||
|
./scripts/install.sh --skip-build --release-id existing-build
|
||||||
|
```
|
||||||
|
|
||||||
|
The script works from any directory and preserves your user configuration.
|
||||||
|
Use `--help` for all options. To test and install manually:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test -p ferese -p feresectl -p ferese-animation -p ferese-core -p ferese-layout -p ferese-shell --locked
|
cargo test -p ferese -p feresectl -p ferese-animation -p ferese-core -p ferese-layout -p ferese-shell --locked
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build as the invoking user; elevate only the versioned system installation.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/install.sh [options]
|
||||||
|
|
||||||
|
Build and install Ferese's compositor, shell, CLI, and login-screen session.
|
||||||
|
Run from any directory as your normal user. Existing user config is preserved.
|
||||||
|
|
||||||
|
--release-id ID Name the installed release (default: UTC timestamp + PID)
|
||||||
|
--skip-build Install existing target/release binaries
|
||||||
|
--offline Build using only cached Cargo dependencies
|
||||||
|
--dry-run Print commands without building or installing
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
The previous release remains available for rollback. Administrator
|
||||||
|
authentication is requested only after a successful build.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() { echo "ferese installer: $*" >&2; exit 1; }
|
||||||
|
repo_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
release_id="$(date -u +%Y%m%d-%H%M%S)-$$"
|
||||||
|
skip_build=false
|
||||||
|
offline=false
|
||||||
|
dry_run=false
|
||||||
|
while (($#)); do
|
||||||
|
case $1 in
|
||||||
|
--release-id)
|
||||||
|
(($# >= 2)) || fail '--release-id requires a value'
|
||||||
|
release_id=$2
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--skip-build) skip_build=true; shift ;;
|
||||||
|
--offline) offline=true; shift ;;
|
||||||
|
--dry-run) dry_run=true; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) fail "unknown option: $1 (see --help)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ $release_id =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || fail 'invalid release ID'
|
||||||
|
[[ $(uname -s) == Linux ]] || fail 'Ferese requires Linux'
|
||||||
|
for tool in desktop-file-validate dbus-run-session; do
|
||||||
|
command -v "$tool" >/dev/null || fail "required command not found: $tool"
|
||||||
|
done
|
||||||
|
desktop-file-validate "$repo_dir/packaging/ferese.desktop"
|
||||||
|
|
||||||
|
build=(cargo build --manifest-path "$repo_dir/Cargo.toml"
|
||||||
|
--target-dir "$repo_dir/target" --release --locked
|
||||||
|
-p ferese -p ferese-shell -p feresectl)
|
||||||
|
if $offline; then build+=(--offline); fi
|
||||||
|
if ! $skip_build; then
|
||||||
|
((EUID != 0)) || fail 'build as your normal user, without sudo (or use --skip-build)'
|
||||||
|
command -v cargo >/dev/null || fail 'install the Rust toolchain and Cargo first'
|
||||||
|
fi
|
||||||
|
|
||||||
|
installer=(bash "$repo_dir/scripts/install-session.sh" "$release_id")
|
||||||
|
if ((EUID != 0)); then
|
||||||
|
if command -v sudo >/dev/null; then
|
||||||
|
installer=(sudo "${installer[@]}")
|
||||||
|
elif command -v pkexec >/dev/null; then
|
||||||
|
installer=(pkexec /usr/bin/bash "$repo_dir/scripts/install-session.sh" "$release_id")
|
||||||
|
else
|
||||||
|
fail 'system installation requires sudo or pkexec'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $dry_run; then
|
||||||
|
if ! $skip_build; then printf 'Build: '; printf '%q ' "${build[@]}"; printf '\n'; fi
|
||||||
|
printf 'Install: '; printf '%q ' "${installer[@]}"; printf '\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd -- "$repo_dir"
|
||||||
|
if ! $skip_build; then "${build[@]}"; fi
|
||||||
|
for name in ferese ferese-shell feresectl; do
|
||||||
|
[[ -x $repo_dir/target/release/$name ]] || fail "missing release binary: $name"
|
||||||
|
done
|
||||||
|
"${installer[@]}"
|
||||||
|
echo 'Installation complete. Log out and select Ferese at the login screen.'
|
||||||
@@ -10,7 +10,7 @@ ferese-protocols = { path = "../../crates/ferese-protocols", default-features =
|
|||||||
jiff.workspace = true
|
jiff.workspace = true
|
||||||
# Keep GPU rendering available as a fallback/override. The shell defaults to
|
# Keep GPU rendering available as a fallback/override. The shell defaults to
|
||||||
# tiny-skia to avoid a second GPU initialization during desktop startup.
|
# tiny-skia to avoid a second GPU initialization during desktop startup.
|
||||||
libcosmic = { workspace = true, features = ["wgpu"] }
|
libcosmic = { workspace = true, features = ["wgpu", "a11y"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
wayland-client.workspace = true
|
wayland-client.workspace = true
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery 25">
|
||||||
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
|
<rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||||
<path d="M22 10v4"/>
|
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
|
||||||
<rect x="5" y="9" width="3" height="6" rx="0.9" fill="currentColor" stroke="none"/>
|
<rect x="5" y="9" width="4.75" height="6" rx="1.2" fill="#e0654f"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 344 B |
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery 50">
|
||||||
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
|
<rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||||
<path d="M22 10v4"/>
|
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
|
||||||
<rect x="5" y="9" width="6" height="6" rx="0.9" fill="currentColor" stroke="none"/>
|
<rect x="5" y="9" width="9.5" height="6" rx="1.2" fill="#3d7be6"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 343 B |
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery 75">
|
||||||
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
|
<rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||||
<path d="M22 10v4"/>
|
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
|
||||||
<rect x="5" y="9" width="9" height="6" rx="0.9" fill="currentColor" stroke="none"/>
|
<rect x="5" y="9" width="14.25" height="6" rx="1.2" fill="#3d7be6"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 345 B |
@@ -1,5 +1,6 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery charging">
|
||||||
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
|
<rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||||
<path d="M22 10v4"/>
|
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
|
||||||
<path d="M12.2 7.8 8.1 12.7h3l-1 3.5 4.5-5.2h-3Z" fill="currentColor" stroke="none"/>
|
<rect x="5" y="9" width="19" height="6" rx="1.2" fill="#3d7be6" opacity="0.35"/>
|
||||||
|
<path d="M16,7 L12,13 H15 L14,17 L19,11 H16 Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 432 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery empty">
|
||||||
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
|
<rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||||
<path d="M22 10v4"/>
|
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 279 B |
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery full">
|
||||||
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
|
<rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||||
<path d="M22 10v4"/>
|
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
|
||||||
<rect x="5" y="9" width="12" height="6" rx="0.9" fill="currentColor" stroke="none"/>
|
<rect x="5" y="9" width="19" height="6" rx="1.2" fill="#63c168"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 344 B |
@@ -1,5 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth">
|
||||||
<path d="M7.5 7.5l10 9L12 21V3l5.5 4.5-10 9"/>
|
<g transform="translate(-11 -4)"><path d="M30,10 L30,58 M30,10 L42,22 L30,34 L42,46 L30,58" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></g>
|
||||||
<circle cx="3.5" cy="12" r="1.1" fill="currentColor" stroke="none"/>
|
<g transform="translate(-11 -4)"><circle cx="49" cy="34" r="4" fill="#3d7be6"/></g>
|
||||||
<circle cx="20.5" cy="12" r="1.1" fill="currentColor" stroke="none"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 383 B After Width: | Height: | Size: 382 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth off">
|
||||||
<path d="M12 3l5.5 4.5-3.2 2.9M12 3v5M7.5 16.5l2.7-2.4M12 16v5l3.5-2.9"/>
|
<g transform="translate(-6 -4)"><path d="M30,10 L30,58 M30,10 L42,22 L30,34 L42,46 L30,58" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/></g>
|
||||||
<path d="M4 4l16 16"/>
|
<path d="M8,8 L52,52" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 413 B |
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth">
|
||||||
<path d="M7.5 7.5l10 9L12 21V3l5.5 4.5-10 9"/>
|
<g transform="translate(-6 -4)"><path d="M30,10 L30,58 M30,10 L42,22 L30,34 L42,46 L30,58" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 297 B |
@@ -1,6 +1,8 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Control center">
|
||||||
<rect x="3" y="4" width="18" height="6" rx="3"/>
|
<g transform="translate(0 -4)">
|
||||||
<rect x="3" y="14" width="18" height="6" rx="3"/>
|
<rect x="10" y="14" width="40" height="16" rx="8" fill="none" stroke="currentColor" stroke-width="3"/>
|
||||||
<circle cx="7" cy="7" r="1.35" fill="currentColor" stroke="none"/>
|
<circle cx="38" cy="22" r="6" fill="#3d7be6"/>
|
||||||
<circle cx="17" cy="17" r="1.35" fill="currentColor" stroke="none"/>
|
<rect x="10" y="38" width="40" height="16" rx="8" fill="none" stroke="currentColor" stroke-width="3"/>
|
||||||
|
<circle cx="22" cy="46" r="6" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 449 B |
@@ -1,4 +1,8 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Notification off">
|
||||||
<path d="M10.3 4.8A5.5 5.5 0 0 1 17.5 10v3M6.7 8.6a5.5 5.5 0 0 0-.2 1.4v4.5L5 17h10M10 20a2.3 2.3 0 0 0 4 0"/>
|
<g transform="translate(0 -2)">
|
||||||
<path d="M4 4l16 16"/>
|
<path d="M30,10 A16,16 0 0,1 46,26 L46,38 L14,38 L14,26 A16,16 0 0,1 30,10 Z" fill="currentColor" opacity="0.5"/>
|
||||||
|
<rect x="10" y="38" width="40" height="5" rx="2" fill="currentColor" opacity="0.5"/>
|
||||||
|
<circle cx="30" cy="50" r="4" fill="currentColor" opacity="0.5"/>
|
||||||
|
<path d="M8,8 L52,56" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 329 B After Width: | Height: | Size: 509 B |
@@ -1,4 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Notification">
|
||||||
<path d="M5 17h14l-1.5-2.5V10a5.5 5.5 0 0 0-11 0v4.5Z"/>
|
<g transform="translate(0 -2)">
|
||||||
<path d="M10 20a2.3 2.3 0 0 0 4 0M12 3v1.5"/>
|
<path d="M30,10 A16,16 0 0,1 46,26 L46,38 L14,38 L14,26 A16,16 0 0,1 30,10 Z" fill="currentColor"/>
|
||||||
|
<rect x="10" y="38" width="40" height="5" rx="2" fill="currentColor"/>
|
||||||
|
<circle cx="30" cy="50" r="4" fill="#3d7be6"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 360 B |
@@ -1,5 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
|
||||||
<path d="M3 9h4l5-4v14l-5-4H3Z"/>
|
<g transform="translate(3 0)">
|
||||||
<path d="M15.2 8.5a5 5 0 0 1 0 7"/>
|
<path d="M27.5,12.5 L15,22.5 L5,22.5 L5,37.5 L15,37.5 L27.5,47.5 Z" fill="currentColor"/>
|
||||||
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13"/>
|
<path d="M34,22 Q40,30 34,38" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M40,15 Q50,30 40,45" fill="none" stroke="#3d7be6" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 308 B After Width: | Height: | Size: 431 B |
@@ -1,5 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
|
||||||
<path d="M3 9h4l5-4v14l-5-4H3Z"/>
|
<g transform="translate(3 0)">
|
||||||
<path d="M15.2 8.5a5 5 0 0 1 0 7" opacity="0.24"/>
|
<path d="M27.5,12.5 L15,22.5 L5,22.5 L5,37.5 L15,37.5 L27.5,47.5 Z" fill="currentColor"/>
|
||||||
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13" opacity="0.24"/>
|
<path d="M34,22 Q40,30 34,38" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M40,15 Q50,30 40,45" fill="none" opacity="0.18" stroke="#3d7be6" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 446 B |
@@ -1,5 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
|
||||||
<path d="M3 9h4l5-4v14l-5-4H3Z"/>
|
<g transform="translate(3 0)">
|
||||||
<path d="M15.2 8.5a5 5 0 0 1 0 7"/>
|
<path d="M27.5,12.5 L15,22.5 L5,22.5 L5,37.5 L15,37.5 L27.5,47.5 Z" fill="currentColor"/>
|
||||||
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13" opacity="0.24"/>
|
<path d="M34,22 Q40,30 34,38" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M40,15 Q50,30 40,45" fill="none" opacity="0.35" stroke="#3d7be6" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 446 B |
@@ -1,4 +1,6 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio off">
|
||||||
<path d="M3 9h4l5-4v14l-5-4H3Z"/>
|
<g transform="translate(3 0)">
|
||||||
<path d="M16 9l5 6m0-6-5 6"/>
|
<path d="M27.5,12.5 L15,22.5 L5,22.5 L5,37.5 L15,37.5 L27.5,47.5 Z" fill="currentColor" opacity="0.5"/>
|
||||||
|
<path d="M6,6 L54,54" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 340 B |
@@ -1,6 +1,8 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
|
||||||
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0"/>
|
<g transform="translate(3 -1.5) scale(0.9)">
|
||||||
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0"/>
|
<circle cx="30" cy="53" r="3" fill="#3d7be6"/>
|
||||||
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/>
|
<path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/>
|
<path d="M12,35 Q30,15 48,35" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M3,26 Q30,5 57,26" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 394 B After Width: | Height: | Size: 510 B |
@@ -1,6 +1,8 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
|
||||||
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0" opacity="0.24"/>
|
<g transform="translate(3 -1.5) scale(0.9)">
|
||||||
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0" opacity="0.24"/>
|
<circle cx="30" cy="53" r="3" fill="#3d7be6"/>
|
||||||
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/>
|
<path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/>
|
<path opacity="0.25" d="M12,35 Q30,15 48,35" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path opacity="0.25" d="M3,26 Q30,5 57,26" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 540 B |
@@ -1,6 +1,8 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
|
||||||
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0" opacity="0.24"/>
|
<g transform="translate(3 -1.5) scale(0.9)">
|
||||||
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0"/>
|
<circle cx="30" cy="53" r="3" fill="#3d7be6"/>
|
||||||
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/>
|
<path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/>
|
<path d="M12,35 Q30,15 48,35" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path opacity="0.25" d="M3,26 Q30,5 57,26" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 409 B After Width: | Height: | Size: 525 B |
@@ -1,5 +1,9 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi off">
|
||||||
<path d="M2.5 7.8a14.5 14.5 0 0 1 2.2-1.5M9 4.8a14.5 14.5 0 0 1 12.5 3M5.7 11.4a9.6 9.6 0 0 1 2.1-1.3M13.5 9.1a9.6 9.6 0 0 1 4.8 2.3M8.9 15a4.7 4.7 0 0 1 6.2 0"/>
|
<g transform="translate(3 -1.5) scale(0.9)">
|
||||||
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/>
|
<circle cx="30" cy="53" r="3" fill="currentColor" opacity="0.5"/>
|
||||||
<path d="M4 4l16 16"/>
|
<path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" opacity="0.5"/>
|
||||||
|
<path d="M12,35 Q30,15 48,35" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" opacity="0.5"/>
|
||||||
|
<path d="M3,26 Q30,5 57,26" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" opacity="0.5"/>
|
||||||
|
<path d="M6,6 L54,54" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 453 B After Width: | Height: | Size: 673 B |
@@ -41,6 +41,7 @@ pub(crate) struct WorkspaceSnapshot {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct WindowSnapshot {
|
pub(crate) struct WindowSnapshot {
|
||||||
|
pub(crate) id: u64,
|
||||||
pub(crate) workspace: u64,
|
pub(crate) workspace: u64,
|
||||||
pub(crate) app_id: String,
|
pub(crate) app_id: String,
|
||||||
pub(crate) title: String,
|
pub(crate) title: String,
|
||||||
@@ -120,6 +121,12 @@ impl ShellControl {
|
|||||||
let _ = self.connection.flush();
|
let _ = self.connection.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn activate_window(&self, id: u64) {
|
||||||
|
let (hi, lo) = split_id(id);
|
||||||
|
self.shell.activate_window(hi, lo);
|
||||||
|
let _ = self.connection.flush();
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_overview_active(&self, active: bool) {
|
pub(crate) fn set_overview_active(&self, active: bool) {
|
||||||
if active {
|
if active {
|
||||||
self.shell.enter_overview();
|
self.shell.enter_overview();
|
||||||
@@ -218,8 +225,8 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ferese_shell_v1::Event::Window {
|
ferese_shell_v1::Event::Window {
|
||||||
window_hi: _,
|
window_hi,
|
||||||
window_lo: _,
|
window_lo,
|
||||||
workspace_hi,
|
workspace_hi,
|
||||||
workspace_lo,
|
workspace_lo,
|
||||||
app_id,
|
app_id,
|
||||||
@@ -235,6 +242,7 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
state.pending.windows.push(WindowSnapshot {
|
state.pending.windows.push(WindowSnapshot {
|
||||||
|
id: join_id(window_hi, window_lo),
|
||||||
workspace: join_id(workspace_hi, workspace_lo),
|
workspace: join_id(workspace_hi, workspace_lo),
|
||||||
app_id,
|
app_id,
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ enum Message {
|
|||||||
),
|
),
|
||||||
Tick,
|
Tick,
|
||||||
ActivateWorkspace(u64),
|
ActivateWorkspace(u64),
|
||||||
|
ActivateWindow(u64),
|
||||||
ToggleOverview,
|
ToggleOverview,
|
||||||
StatusTick,
|
StatusTick,
|
||||||
AnimateMenu,
|
AnimateMenu,
|
||||||
@@ -383,6 +384,12 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::ActivateWindow(id) => {
|
||||||
|
if let Some(control) = &self.control {
|
||||||
|
control.activate_window(id);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::ActivateWorkspace(id) => {
|
Message::ActivateWorkspace(id) => {
|
||||||
if let Some(control) = &self.control {
|
if let Some(control) = &self.control {
|
||||||
control.activate_workspace(id);
|
control.activate_workspace(id);
|
||||||
@@ -597,32 +604,41 @@ impl FereseShell {
|
|||||||
workspace_row = workspace_row.push(workspace_button);
|
workspace_row = workspace_row.push(workspace_button);
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_name = self
|
|
||||||
.snapshot
|
|
||||||
.windows
|
|
||||||
.iter()
|
|
||||||
.find(|window| {
|
|
||||||
window.focused
|
|
||||||
&& focused_output
|
|
||||||
.is_none_or(|output| window.workspace == output.active_workspace)
|
|
||||||
})
|
|
||||||
.map(display_app_name)
|
|
||||||
.unwrap_or_else(|| "Desktop".to_owned());
|
|
||||||
let foreground = color(shell_theme.text_primary);
|
let foreground = color(shell_theme.text_primary);
|
||||||
let app_name = if self.overview_active {
|
let mut windows = row::with_capacity(self.snapshot.windows.len())
|
||||||
"Overview".to_owned()
|
.spacing(4)
|
||||||
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
|
for window in self.snapshot.windows.iter().filter(|window| {
|
||||||
|
focused_output.is_some_and(|output| window.workspace == output.active_workspace)
|
||||||
|
}) {
|
||||||
|
let label = compact_window_label(window);
|
||||||
|
let foreground = color(if window.focused {
|
||||||
|
shell_theme.text_primary
|
||||||
} else {
|
} else {
|
||||||
app_name
|
shell_theme.text_muted
|
||||||
};
|
});
|
||||||
let left = row![
|
windows = windows.push(
|
||||||
workspace_row,
|
button::custom(
|
||||||
text(app_name)
|
text(label)
|
||||||
.size(bar.text_size)
|
.size(bar.text_size)
|
||||||
.font(cosmic::font::semibold())
|
|
||||||
.width(Length::Fill)
|
|
||||||
.wrapping(cosmic::iced::core::text::Wrapping::None)
|
.wrapping(cosmic::iced::core::text::Wrapping::None)
|
||||||
.class(theme::Text::Color(foreground))
|
.class(theme::Text::Color(foreground)),
|
||||||
]
|
)
|
||||||
|
.padding([0, 8])
|
||||||
|
.height(bar.control_height)
|
||||||
|
.class(status_ui::button_style(foreground, window.focused, 1.0))
|
||||||
|
.on_press(cosmic::Action::App(Message::ActivateWindow(window.id))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let windows = cosmic::iced::widget::scrollable(windows)
|
||||||
|
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
|
||||||
|
cosmic::iced::widget::scrollable::Scrollbar::default()
|
||||||
|
.width(0)
|
||||||
|
.scroller_width(0),
|
||||||
|
))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(bar.control_height);
|
||||||
|
let left = row![workspace_row, windows]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(cosmic::iced::Alignment::Center);
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
let clock = container(
|
let clock = container(
|
||||||
@@ -680,23 +696,49 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn symbolic_icon(source: &'static [u8], size: u16) -> icon::Icon {
|
fn bar_icon(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon {
|
||||||
icon::from_svg_bytes(source)
|
accented_icon(source, size, foreground, foreground)
|
||||||
.symbolic(true)
|
|
||||||
.icon()
|
|
||||||
.size(size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bar_icon(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon {
|
fn accented_icon(source: &'static [u8], size: u16, foreground: Color, accent: Color) -> icon::Icon {
|
||||||
// COSMIC's transparent containers replace inherited foreground colors.
|
let svg = std::str::from_utf8(source).expect("embedded SVG must be UTF-8");
|
||||||
// Pin the bar glyph color rather than relying on those container defaults.
|
// Battery canvases are wider; reserve that space instead of stretching
|
||||||
symbolic_icon(source, size).class(theme::Svg::custom(move |_| {
|
// or clipping them into the square slot used by the other status icons.
|
||||||
cosmic::iced::widget::svg::Style {
|
let aspect = if svg.contains("viewBox=\"0 0 32 24\"") {
|
||||||
color: Some(foreground),
|
4.0 / 3.0
|
||||||
}
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
icon::from_svg_bytes(tinted_svg(source, foreground, accent))
|
||||||
|
.symbolic(false)
|
||||||
|
.icon()
|
||||||
|
.size(size)
|
||||||
|
.width(Length::Fixed(f32::from(size) * aspect))
|
||||||
|
.content_fit(ContentFit::Contain)
|
||||||
|
.class(theme::Svg::custom(move |_| {
|
||||||
|
cosmic::iced::widget::svg::Style { color: None }
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tinted_svg(source: &[u8], foreground: Color, accent: Color) -> Vec<u8> {
|
||||||
|
let rgba = |foreground: Color| {
|
||||||
|
format!(
|
||||||
|
"rgba({},{},{},{})",
|
||||||
|
(foreground.r * 255.0).round() as u8,
|
||||||
|
(foreground.g * 255.0).round() as u8,
|
||||||
|
(foreground.b * 255.0).round() as u8,
|
||||||
|
foreground.a,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
std::str::from_utf8(source)
|
||||||
|
.expect("embedded icons must be UTF-8 SVG")
|
||||||
|
.replace("currentColor", &rgba(foreground))
|
||||||
|
// The source artwork's blue swatch is the semantic theme accent.
|
||||||
|
// Battery warning/success swatches remain status colors.
|
||||||
|
.replace("#3d7be6", &rgba(accent))
|
||||||
|
.into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
fn overview_control(
|
fn overview_control(
|
||||||
bar: BarMetrics,
|
bar: BarMetrics,
|
||||||
foreground: Color,
|
foreground: Color,
|
||||||
@@ -806,6 +848,22 @@ fn current_time() -> String {
|
|||||||
Zoned::now().strftime("%a · %H:%M").to_string()
|
Zoned::now().strftime("%a · %H:%M").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compact_window_label(window: &crate::control::WindowSnapshot) -> String {
|
||||||
|
let title = window.title.trim();
|
||||||
|
let label = if title.is_empty() {
|
||||||
|
display_app_name(window)
|
||||||
|
} else {
|
||||||
|
title.to_owned()
|
||||||
|
};
|
||||||
|
let mut characters = label.chars();
|
||||||
|
let prefix: String = characters.by_ref().take(24).collect();
|
||||||
|
if characters.next().is_some() {
|
||||||
|
format!("{prefix}…")
|
||||||
|
} else {
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn shell_surface_style(theme: ShellTheme) -> cosmic::iced::theme::Style {
|
fn shell_surface_style(theme: ShellTheme) -> cosmic::iced::theme::Style {
|
||||||
cosmic::iced::theme::Style {
|
cosmic::iced::theme::Style {
|
||||||
background_color: Color::TRANSPARENT,
|
background_color: Color::TRANSPARENT,
|
||||||
@@ -967,6 +1025,70 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::control::{OutputSnapshot, WindowSnapshot};
|
use crate::control::{OutputSnapshot, WindowSnapshot};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn icons_use_theme_accent_and_preserve_battery_status_colors() {
|
||||||
|
for (source, accent) in [
|
||||||
|
(
|
||||||
|
include_bytes!("../assets/icons/status/battery-full.svg").as_slice(),
|
||||||
|
"#63c168",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
include_bytes!("../assets/icons/status/battery-50.svg").as_slice(),
|
||||||
|
"#3d7be6",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
include_bytes!("../assets/icons/status/battery-25.svg").as_slice(),
|
||||||
|
"#e0654f",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
include_bytes!("../assets/icons/status/wifi-full.svg").as_slice(),
|
||||||
|
"#3d7be6",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
include_bytes!("../assets/icons/status/control-center.svg").as_slice(),
|
||||||
|
"#3d7be6",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let svg = String::from_utf8(tinted_svg(
|
||||||
|
source,
|
||||||
|
Color::from_rgb8(205, 214, 244),
|
||||||
|
Color::from_rgb8(203, 166, 247),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(!svg.contains("currentColor"));
|
||||||
|
assert!(svg.contains("rgba(205,214,244,1)"));
|
||||||
|
assert!(!svg.contains("#3d7be6"));
|
||||||
|
assert!(svg.contains(if accent == "#3d7be6" {
|
||||||
|
"rgba(203,166,247,1)"
|
||||||
|
} else {
|
||||||
|
accent
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_list_labels_use_titles_and_fall_back_to_the_app_name() {
|
||||||
|
let snapshot = snapshot_with_fullscreen_window(7);
|
||||||
|
let mut window = snapshot.windows[0].clone();
|
||||||
|
window.title = " editor.rs — Project ".to_owned();
|
||||||
|
assert_eq!(compact_window_label(&window), "editor.rs — Project");
|
||||||
|
window.title = " ".to_owned();
|
||||||
|
assert_eq!(compact_window_label(&window), display_app_name(&window));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_list_labels_truncate_unicode_without_breaking_characters() {
|
||||||
|
let snapshot = snapshot_with_fullscreen_window(7);
|
||||||
|
let mut window = snapshot.windows[0].clone();
|
||||||
|
window.title = "界".repeat(25);
|
||||||
|
assert_eq!(
|
||||||
|
compact_window_label(&window),
|
||||||
|
format!("{}…", "界".repeat(24))
|
||||||
|
);
|
||||||
|
window.title = "界".repeat(24);
|
||||||
|
assert_eq!(compact_window_label(&window), window.title);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn startup_surface_clear_is_transparent_and_bar_fallback_is_dark() {
|
fn startup_surface_clear_is_transparent_and_bar_fallback_is_dark() {
|
||||||
let theme = ShellTheme::default();
|
let theme = ShellTheme::default();
|
||||||
@@ -1048,6 +1170,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
workspaces: Vec::new(),
|
workspaces: Vec::new(),
|
||||||
windows: vec![WindowSnapshot {
|
windows: vec![WindowSnapshot {
|
||||||
|
id: 1,
|
||||||
workspace,
|
workspace,
|
||||||
app_id: "dev.ferese.Test".to_owned(),
|
app_id: "dev.ferese.Test".to_owned(),
|
||||||
title: "Test".to_owned(),
|
title: "Test".to_owned(),
|
||||||
|
|||||||
@@ -152,31 +152,24 @@ impl FereseShell {
|
|||||||
let metrics = BarMetrics::from(theme);
|
let metrics = BarMetrics::from(theme);
|
||||||
let mut controls = row::with_capacity(6).spacing(3).align_y(Alignment::Center);
|
let mut controls = row::with_capacity(6).spacing(3).align_y(Alignment::Center);
|
||||||
for kind in [
|
for kind in [
|
||||||
|
Menu::System,
|
||||||
Menu::Network,
|
Menu::Network,
|
||||||
Menu::Bluetooth,
|
Menu::Bluetooth,
|
||||||
Menu::Audio,
|
Menu::Audio,
|
||||||
Menu::Battery,
|
|
||||||
Menu::Notifications,
|
Menu::Notifications,
|
||||||
Menu::System,
|
Menu::Battery,
|
||||||
] {
|
] {
|
||||||
if !kind.available(&self.status) {
|
if !kind.available(&self.status) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let (source, active) = status_icon(kind, &self.status);
|
let (source, _) = status_icon(kind, &self.status);
|
||||||
let warning = kind == Menu::Battery
|
let foreground = color(theme.text_primary);
|
||||||
&& self.status.battery.as_ref().is_some_and(|b| {
|
let mut content = row![accented_icon(
|
||||||
b.percent < self.config.status.low_battery_threshold && b.status != "Charging"
|
source,
|
||||||
});
|
metrics.icon_size,
|
||||||
let foreground = if warning {
|
foreground,
|
||||||
Color::from_rgb8(245, 193, 105)
|
color(theme.accent)
|
||||||
} else {
|
)]
|
||||||
color(if active {
|
|
||||||
theme.text_primary
|
|
||||||
} else {
|
|
||||||
theme.text_muted
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let mut content = row![bar_icon(source, metrics.icon_size, foreground)]
|
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.align_y(Alignment::Center);
|
.align_y(Alignment::Center);
|
||||||
if kind == Menu::Battery && self.config.status.battery_percentage {
|
if kind == Menu::Battery && self.config.status.battery_percentage {
|
||||||
@@ -210,11 +203,16 @@ impl FereseShell {
|
|||||||
// A fixed button height does not center its child in libcosmic.
|
// A fixed button height does not center its child in libcosmic.
|
||||||
// Keep the visual content centered inside the entire click target.
|
// Keep the visual content centered inside the entire click target.
|
||||||
let content = container(content)
|
let content = container(content)
|
||||||
.height(metrics.control_height)
|
.align_x(alignment::Horizontal::Center)
|
||||||
|
.height(metrics.height)
|
||||||
.align_y(alignment::Vertical::Center);
|
.align_y(alignment::Vertical::Center);
|
||||||
let control = button::custom(content)
|
let control = button::custom(content)
|
||||||
.padding([0, 7])
|
.name(status_label(kind, &self.status))
|
||||||
.height(metrics.control_height)
|
.padding([
|
||||||
|
0.0,
|
||||||
|
((metrics.height - f32::from(metrics.icon_size)) * 0.5).max(7.0),
|
||||||
|
])
|
||||||
|
.height(metrics.height)
|
||||||
.class(button_style(foreground, selected, 1.0))
|
.class(button_style(foreground, selected, 1.0))
|
||||||
.on_press_with_rectangle(move |offset, bounds| {
|
.on_press_with_rectangle(move |offset, bounds| {
|
||||||
cosmic::Action::App(Message::OpenMenu(
|
cosmic::Action::App(Message::OpenMenu(
|
||||||
@@ -365,6 +363,7 @@ impl FereseShell {
|
|||||||
},
|
},
|
||||||
primary,
|
primary,
|
||||||
muted,
|
muted,
|
||||||
|
color_with_opacity(theme.accent, p),
|
||||||
p,
|
p,
|
||||||
));
|
));
|
||||||
if connected {
|
if connected {
|
||||||
@@ -408,6 +407,7 @@ impl FereseShell {
|
|||||||
},
|
},
|
||||||
primary,
|
primary,
|
||||||
muted,
|
muted,
|
||||||
|
color_with_opacity(theme.accent, p),
|
||||||
p,
|
p,
|
||||||
));
|
));
|
||||||
if b.enabled && !b.devices.is_empty() {
|
if b.enabled && !b.devices.is_empty() {
|
||||||
@@ -439,7 +439,14 @@ impl FereseShell {
|
|||||||
]
|
]
|
||||||
.align_y(Alignment::Center),
|
.align_y(Alignment::Center),
|
||||||
text(&a.output).size(12).class(theme::Text::Color(muted)),
|
text(&a.output).size(12).class(theme::Text::Color(muted)),
|
||||||
slider_row(audio_icon(a.volume, a.muted), a.volume, false, primary, p,)
|
slider_row(
|
||||||
|
audio_icon(a.volume, a.muted),
|
||||||
|
a.volume,
|
||||||
|
false,
|
||||||
|
primary,
|
||||||
|
color_with_opacity(theme.accent, p),
|
||||||
|
p,
|
||||||
|
)
|
||||||
]
|
]
|
||||||
.spacing(4);
|
.spacing(4);
|
||||||
rows = rows.push(control_card(audio.into(), primary, p));
|
rows = rows.push(control_card(audio.into(), primary, p));
|
||||||
@@ -454,6 +461,7 @@ impl FereseShell {
|
|||||||
value,
|
value,
|
||||||
true,
|
true,
|
||||||
primary,
|
primary,
|
||||||
|
color_with_opacity(theme.accent, p),
|
||||||
p,
|
p,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -502,7 +510,12 @@ impl FereseShell {
|
|||||||
container(
|
container(
|
||||||
row![
|
row![
|
||||||
text(format!("{}%", b.percent)).size(40).width(Length::Fill),
|
text(format!("{}%", b.percent)).size(40).width(Length::Fill),
|
||||||
symbolic_icon(status_icon(Menu::Battery, &self.status).0, 40),
|
accented_icon(
|
||||||
|
status_icon(Menu::Battery, &self.status).0,
|
||||||
|
40,
|
||||||
|
battery_color,
|
||||||
|
color_with_opacity(theme.accent, p),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.align_y(Alignment::Center),
|
.align_y(Alignment::Center),
|
||||||
)
|
)
|
||||||
@@ -639,9 +652,10 @@ fn status_summary<'a>(
|
|||||||
subtitle: &'a str,
|
subtitle: &'a str,
|
||||||
primary: Color,
|
primary: Color,
|
||||||
muted: Color,
|
muted: Color,
|
||||||
|
accent: Color,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
) -> Element<'a, cosmic::Action<Message>> {
|
) -> Element<'a, cosmic::Action<Message>> {
|
||||||
let badge = container(symbolic_icon(source, 24))
|
let badge = container(accented_icon(source, 24, primary, accent))
|
||||||
.width(48)
|
.width(48)
|
||||||
.height(48)
|
.height(48)
|
||||||
.align_x(alignment::Horizontal::Center)
|
.align_x(alignment::Horizontal::Center)
|
||||||
@@ -742,8 +756,9 @@ fn connection_control<'a>(
|
|||||||
opacity: f32,
|
opacity: f32,
|
||||||
) -> Element<'a, cosmic::Action<Message>> {
|
) -> Element<'a, cosmic::Action<Message>> {
|
||||||
let foreground = color_with_opacity(theme.text_primary, opacity);
|
let foreground = color_with_opacity(theme.text_primary, opacity);
|
||||||
|
let icon_accent = color_with_opacity(theme.accent, opacity);
|
||||||
let accent = color_with_opacity(theme.accent, opacity * 0.14);
|
let accent = color_with_opacity(theme.accent, opacity * 0.14);
|
||||||
let icon = container(symbolic_icon(source, 18))
|
let icon = container(accented_icon(source, 18, foreground, icon_accent))
|
||||||
.width(32)
|
.width(32)
|
||||||
.height(32)
|
.height(32)
|
||||||
.align_x(alignment::Horizontal::Center)
|
.align_x(alignment::Horizontal::Center)
|
||||||
@@ -850,6 +865,7 @@ fn slider_row(
|
|||||||
value: u8,
|
value: u8,
|
||||||
brightness: bool,
|
brightness: bool,
|
||||||
foreground: Color,
|
foreground: Color,
|
||||||
|
icon_accent: Color,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
) -> Element<'static, cosmic::Action<Message>> {
|
) -> Element<'static, cosmic::Action<Message>> {
|
||||||
let style = std::rc::Rc::new(move |_: &cosmic::Theme| {
|
let style = std::rc::Rc::new(move |_: &cosmic::Theme| {
|
||||||
@@ -900,7 +916,7 @@ fn slider_row(
|
|||||||
dragging: style,
|
dragging: style,
|
||||||
});
|
});
|
||||||
row![
|
row![
|
||||||
symbolic_icon(source, 18),
|
accented_icon(source, 18, foreground, icon_accent),
|
||||||
control,
|
control,
|
||||||
text(format!("{value}%")).size(12).width(36)
|
text(format!("{value}%")).size(12).width(36)
|
||||||
]
|
]
|
||||||
@@ -909,6 +925,36 @@ fn slider_row(
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn status_label(kind: Menu, s: &Snapshot) -> String {
|
||||||
|
match kind {
|
||||||
|
Menu::Network => match &s.network {
|
||||||
|
Some(n) if n.enabled => format!(
|
||||||
|
"Wi-Fi: {}",
|
||||||
|
n.connection.as_deref().unwrap_or("not connected")
|
||||||
|
),
|
||||||
|
_ => "Wi-Fi: off".into(),
|
||||||
|
},
|
||||||
|
Menu::Bluetooth => match &s.bluetooth {
|
||||||
|
Some(b) if b.enabled => format!("Bluetooth: on, {} devices connected", b.devices.len()),
|
||||||
|
_ => "Bluetooth: off".into(),
|
||||||
|
},
|
||||||
|
Menu::Audio => match &s.audio {
|
||||||
|
Some(a) if !a.muted && a.volume > 0 => format!("Audio: {}%", a.volume),
|
||||||
|
_ => "Audio: muted".into(),
|
||||||
|
},
|
||||||
|
Menu::Notifications => match &s.notifications {
|
||||||
|
Some(n) if n.dnd => "Notifications: do not disturb".into(),
|
||||||
|
Some(n) => format!("Notifications: {} unread", n.count),
|
||||||
|
_ => "Notifications: unavailable".into(),
|
||||||
|
},
|
||||||
|
Menu::Battery => s.battery.as_ref().map_or_else(
|
||||||
|
|| "Battery: unavailable".into(),
|
||||||
|
|b| format!("Battery: {}%, {}", b.percent, b.status),
|
||||||
|
),
|
||||||
|
Menu::System => "Control Center".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn audio_icon(volume: u8, muted: bool) -> &'static [u8] {
|
fn audio_icon(volume: u8, muted: bool) -> &'static [u8] {
|
||||||
if muted || volume == 0 {
|
if muted || volume == 0 {
|
||||||
include_bytes!("../assets/icons/status/volume-mute.svg")
|
include_bytes!("../assets/icons/status/volume-mute.svg")
|
||||||
@@ -940,7 +986,7 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Menu::Bluetooth => match &s.bluetooth {
|
Menu::Bluetooth => match &s.bluetooth {
|
||||||
Some(b) if !b.devices.is_empty() => (
|
Some(b) if b.enabled && !b.devices.is_empty() => (
|
||||||
include_bytes!("../assets/icons/status/bluetooth-connected.svg"),
|
include_bytes!("../assets/icons/status/bluetooth-connected.svg"),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
@@ -965,13 +1011,13 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
|||||||
};
|
};
|
||||||
let icon: &'static [u8] = if b.status == "Charging" {
|
let icon: &'static [u8] = if b.status == "Charging" {
|
||||||
include_bytes!("../assets/icons/status/battery-charging.svg")
|
include_bytes!("../assets/icons/status/battery-charging.svg")
|
||||||
} else if b.percent > 87 {
|
} else if b.percent >= 80 {
|
||||||
include_bytes!("../assets/icons/status/battery-full.svg")
|
include_bytes!("../assets/icons/status/battery-full.svg")
|
||||||
} else if b.percent > 62 {
|
} else if b.percent >= 50 {
|
||||||
include_bytes!("../assets/icons/status/battery-75.svg")
|
include_bytes!("../assets/icons/status/battery-75.svg")
|
||||||
} else if b.percent > 37 {
|
} else if b.percent >= 20 {
|
||||||
include_bytes!("../assets/icons/status/battery-50.svg")
|
include_bytes!("../assets/icons/status/battery-50.svg")
|
||||||
} else if b.percent > 12 {
|
} else if b.percent > 0 {
|
||||||
include_bytes!("../assets/icons/status/battery-25.svg")
|
include_bytes!("../assets/icons/status/battery-25.svg")
|
||||||
} else {
|
} else {
|
||||||
include_bytes!("../assets/icons/status/battery-empty.svg")
|
include_bytes!("../assets/icons/status/battery-empty.svg")
|
||||||
@@ -980,7 +1026,10 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
|||||||
}
|
}
|
||||||
Menu::Notifications => {
|
Menu::Notifications => {
|
||||||
if s.notifications.as_ref().is_some_and(|n| n.dnd) {
|
if s.notifications.as_ref().is_some_and(|n| n.dnd) {
|
||||||
(include_bytes!("../assets/icons/status/dnd.svg"), false)
|
(
|
||||||
|
include_bytes!("../assets/icons/status/notifications-off.svg"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
include_bytes!("../assets/icons/status/notifications.svg"),
|
include_bytes!("../assets/icons/status/notifications.svg"),
|
||||||
@@ -1042,5 +1091,62 @@ mod tests {
|
|||||||
status_icon(Menu::Battery, &s).0,
|
status_icon(Menu::Battery, &s).0,
|
||||||
include_bytes!("../assets/icons/status/battery-charging.svg")
|
include_bytes!("../assets/icons/status/battery-charging.svg")
|
||||||
);
|
);
|
||||||
|
s.notifications = Some(status::Notifications {
|
||||||
|
count: 3,
|
||||||
|
dnd: true,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
status_icon(Menu::Notifications, &s).0,
|
||||||
|
include_bytes!("../assets/icons/status/notifications-off.svg")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
status_label(Menu::Notifications, &s),
|
||||||
|
"Notifications: do not disturb"
|
||||||
|
);
|
||||||
|
s.bluetooth = Some(status::Bluetooth {
|
||||||
|
enabled: false,
|
||||||
|
devices: vec!["Headphones".into()],
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
status_icon(Menu::Bluetooth, &s).0,
|
||||||
|
include_bytes!("../assets/icons/status/bluetooth-off.svg")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn battery_bands_and_live_label_follow_charge_state() {
|
||||||
|
let mut s = Snapshot::default();
|
||||||
|
for (percent, expected) in [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
include_bytes!("../assets/icons/status/battery-empty.svg").as_slice(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
19,
|
||||||
|
include_bytes!("../assets/icons/status/battery-25.svg").as_slice(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
20,
|
||||||
|
include_bytes!("../assets/icons/status/battery-50.svg").as_slice(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
50,
|
||||||
|
include_bytes!("../assets/icons/status/battery-75.svg").as_slice(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
80,
|
||||||
|
include_bytes!("../assets/icons/status/battery-full.svg").as_slice(),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
s.battery = Some(status::Battery {
|
||||||
|
percent,
|
||||||
|
status: "Discharging".into(),
|
||||||
|
});
|
||||||
|
assert_eq!(status_icon(Menu::Battery, &s).0, expected);
|
||||||
|
assert_eq!(
|
||||||
|
status_label(Menu::Battery, &s),
|
||||||
|
format!("Battery: {percent}%, Discharging")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||