Refine shell status icons and window controls; add installer

This commit is contained in:
Samuel Onoja committed 2026-09-26 05:46:15 +01:00
1 parent 956c739940
commit 686b0477d4
28 files changed
+527 -155

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ ferese-protocols = { path = "../../crates/ferese-protocols", default-features =
jiff.workspace = true
# Keep GPU rendering available as a fallback/override. The shell defaults to
# 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
toml.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">
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
<path d="M22 10v4"/>
<rect x="5" y="9" width="3" height="6" rx="0.9" fill="currentColor" stroke="none"/>
<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="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<rect x="5" y="9" width="4.75" height="6" rx="1.2" fill="#e0654f"/>
</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">
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
<path d="M22 10v4"/>
<rect x="5" y="9" width="6" height="6" rx="0.9" fill="currentColor" stroke="none"/>
<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="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<rect x="5" y="9" width="9.5" height="6" rx="1.2" fill="#3d7be6"/>
</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">
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
<path d="M22 10v4"/>
<rect x="5" y="9" width="9" height="6" rx="0.9" fill="currentColor" stroke="none"/>
<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="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<rect x="5" y="9" width="14.25" height="6" rx="1.2" fill="#3d7be6"/>
</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">
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
<path d="M22 10v4"/>
<path d="M12.2 7.8 8.1 12.7h3l-1 3.5 4.5-5.2h-3Z" fill="currentColor" stroke="none"/>
<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="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<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>

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">
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
<path d="M22 10v4"/>
<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="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
</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">
<rect x="2" y="6" width="18" height="12" rx="2.8"/>
<path d="M22 10v4"/>
<rect x="5" y="9" width="12" height="6" rx="0.9" fill="currentColor" stroke="none"/>
<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="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<rect x="5" y="9" width="19" height="6" rx="1.2" fill="#63c168"/>
</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">
<path d="M7.5 7.5l10 9L12 21V3l5.5 4.5-10 9"/>
<circle cx="3.5" cy="12" r="1.1" fill="currentColor" stroke="none"/>
<circle cx="20.5" cy="12" r="1.1" fill="currentColor" stroke="none"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth">
<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>
<g transform="translate(-11 -4)"><circle cx="49" cy="34" r="4" fill="#3d7be6"/></g>
</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">
<path d="M12 3l5.5 4.5-3.2 2.9M12 3v5M7.5 16.5l2.7-2.4M12 16v5l3.5-2.9"/>
<path d="M4 4l16 16"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth off">
<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="M8,8 L52,52" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</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">
<path d="M7.5 7.5l10 9L12 21V3l5.5 4.5-10 9"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth">
<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>

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">
<rect x="3" y="4" width="18" height="6" rx="3"/>
<rect x="3" y="14" width="18" height="6" rx="3"/>
<circle cx="7" cy="7" r="1.35" fill="currentColor" stroke="none"/>
<circle cx="17" cy="17" r="1.35" fill="currentColor" stroke="none"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Control center">
<g transform="translate(0 -4)">
<rect x="10" y="14" width="40" height="16" rx="8" fill="none" stroke="currentColor" stroke-width="3"/>
<circle cx="38" cy="22" r="6" fill="#3d7be6"/>
<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>

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">
<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"/>
<path d="M4 4l16 16"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Notification off">
<g transform="translate(0 -2)">
<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>

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">
<path d="M5 17h14l-1.5-2.5V10a5.5 5.5 0 0 0-11 0v4.5Z"/>
<path d="M10 20a2.3 2.3 0 0 0 4 0M12 3v1.5"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Notification">
<g transform="translate(0 -2)">
<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>

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">
<path d="M3 9h4l5-4v14l-5-4H3Z"/>
<path d="M15.2 8.5a5 5 0 0 1 0 7"/>
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
<g transform="translate(3 0)">
<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="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>

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">
<path d="M3 9h4l5-4v14l-5-4H3Z"/>
<path d="M15.2 8.5a5 5 0 0 1 0 7" opacity="0.24"/>
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13" opacity="0.24"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
<g transform="translate(3 0)">
<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="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>

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">
<path d="M3 9h4l5-4v14l-5-4H3Z"/>
<path d="M15.2 8.5a5 5 0 0 1 0 7"/>
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13" opacity="0.24"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
<g transform="translate(3 0)">
<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="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>

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">
<path d="M3 9h4l5-4v14l-5-4H3Z"/>
<path d="M16 9l5 6m0-6-5 6"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio off">
<g transform="translate(3 0)">
<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>

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">
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0"/>
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0"/>
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/>
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
<g transform="translate(3 -1.5) scale(0.9)">
<circle cx="30" cy="53" r="3" fill="#3d7be6"/>
<path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<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>

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">
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0" opacity="0.24"/>
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0" opacity="0.24"/>
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/>
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
<g transform="translate(3 -1.5) scale(0.9)">
<circle cx="30" cy="53" r="3" fill="#3d7be6"/>
<path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<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>

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">
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0" opacity="0.24"/>
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0"/>
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/>
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
<g transform="translate(3 -1.5) scale(0.9)">
<circle cx="30" cy="53" r="3" fill="#3d7be6"/>
<path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<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>

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">
<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"/>
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/>
<path d="M4 4l16 16"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi off">
<g transform="translate(3 -1.5) scale(0.9)">
<circle cx="30" cy="53" r="3" fill="currentColor" opacity="0.5"/>
<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>

Before

Width:  |  Height:  |  Size: 453 B

After

Width:  |  Height:  |  Size: 673 B

+10 -2
View File
@@ -41,6 +41,7 @@ pub(crate) struct WorkspaceSnapshot {
#[derive(Clone, Debug)]
pub(crate) struct WindowSnapshot {
pub(crate) id: u64,
pub(crate) workspace: u64,
pub(crate) app_id: String,
pub(crate) title: String,
@@ -120,6 +121,12 @@ impl ShellControl {
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) {
if active {
self.shell.enter_overview();
@@ -218,8 +225,8 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
});
}
ferese_shell_v1::Event::Window {
window_hi: _,
window_lo: _,
window_hi,
window_lo,
workspace_hi,
workspace_lo,
app_id,
@@ -235,6 +242,7 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
};
state.pending.windows.push(WindowSnapshot {
id: join_id(window_hi, window_lo),
workspace: join_id(workspace_hi, workspace_lo),
app_id,
title,
+163 -40
View File
@@ -144,6 +144,7 @@ enum Message {
),
Tick,
ActivateWorkspace(u64),
ActivateWindow(u64),
ToggleOverview,
StatusTick,
AnimateMenu,
@@ -383,6 +384,12 @@ impl cosmic::Application for FereseShell {
}
Task::none()
}
Message::ActivateWindow(id) => {
if let Some(control) = &self.control {
control.activate_window(id);
}
Task::none()
}
Message::ActivateWorkspace(id) => {
if let Some(control) = &self.control {
control.activate_workspace(id);
@@ -597,34 +604,43 @@ impl FereseShell {
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 app_name = if self.overview_active {
"Overview".to_owned()
} else {
app_name
};
let left = row![
workspace_row,
text(app_name)
.size(bar.text_size)
.font(cosmic::font::semibold())
.width(Length::Fill)
.wrapping(cosmic::iced::core::text::Wrapping::None)
.class(theme::Text::Color(foreground))
]
.spacing(8)
.align_y(cosmic::iced::Alignment::Center);
let mut windows = row::with_capacity(self.snapshot.windows.len())
.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 {
shell_theme.text_muted
});
windows = windows.push(
button::custom(
text(label)
.size(bar.text_size)
.wrapping(cosmic::iced::core::text::Wrapping::None)
.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)
.align_y(cosmic::iced::Alignment::Center);
let clock = container(
text(&self.clock)
.size(bar.text_size)
@@ -680,21 +696,47 @@ impl FereseShell {
}
}
fn symbolic_icon(source: &'static [u8], size: u16) -> icon::Icon {
icon::from_svg_bytes(source)
.symbolic(true)
.icon()
.size(size)
fn bar_icon(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon {
accented_icon(source, size, foreground, foreground)
}
fn bar_icon(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon {
// COSMIC's transparent containers replace inherited foreground colors.
// Pin the bar glyph color rather than relying on those container defaults.
symbolic_icon(source, size).class(theme::Svg::custom(move |_| {
cosmic::iced::widget::svg::Style {
color: Some(foreground),
}
}))
fn accented_icon(source: &'static [u8], size: u16, foreground: Color, accent: Color) -> icon::Icon {
let svg = std::str::from_utf8(source).expect("embedded SVG must be UTF-8");
// Battery canvases are wider; reserve that space instead of stretching
// or clipping them into the square slot used by the other status icons.
let aspect = if svg.contains("viewBox=\"0 0 32 24\"") {
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(
@@ -806,6 +848,22 @@ fn current_time() -> 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 {
cosmic::iced::theme::Style {
background_color: Color::TRANSPARENT,
@@ -967,6 +1025,70 @@ mod tests {
use super::*;
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]
fn startup_surface_clear_is_transparent_and_bar_fallback_is_dark() {
let theme = ShellTheme::default();
@@ -1048,6 +1170,7 @@ mod tests {
}],
workspaces: Vec::new(),
windows: vec![WindowSnapshot {
id: 1,
workspace,
app_id: "dev.ferese.Test".to_owned(),
title: "Test".to_owned(),
+139 -33
View File
@@ -152,33 +152,26 @@ impl FereseShell {
let metrics = BarMetrics::from(theme);
let mut controls = row::with_capacity(6).spacing(3).align_y(Alignment::Center);
for kind in [
Menu::System,
Menu::Network,
Menu::Bluetooth,
Menu::Audio,
Menu::Battery,
Menu::Notifications,
Menu::System,
Menu::Battery,
] {
if !kind.available(&self.status) {
continue;
}
let (source, active) = status_icon(kind, &self.status);
let warning = kind == Menu::Battery
&& self.status.battery.as_ref().is_some_and(|b| {
b.percent < self.config.status.low_battery_threshold && b.status != "Charging"
});
let foreground = if warning {
Color::from_rgb8(245, 193, 105)
} else {
color(if active {
theme.text_primary
} else {
theme.text_muted
})
};
let mut content = row![bar_icon(source, metrics.icon_size, foreground)]
.spacing(4)
.align_y(Alignment::Center);
let (source, _) = status_icon(kind, &self.status);
let foreground = color(theme.text_primary);
let mut content = row![accented_icon(
source,
metrics.icon_size,
foreground,
color(theme.accent)
)]
.spacing(4)
.align_y(Alignment::Center);
if kind == Menu::Battery && self.config.status.battery_percentage {
if let Some(battery) = &self.status.battery {
content = content.push(
@@ -210,11 +203,16 @@ impl FereseShell {
// A fixed button height does not center its child in libcosmic.
// Keep the visual content centered inside the entire click target.
let content = container(content)
.height(metrics.control_height)
.align_x(alignment::Horizontal::Center)
.height(metrics.height)
.align_y(alignment::Vertical::Center);
let control = button::custom(content)
.padding([0, 7])
.height(metrics.control_height)
.name(status_label(kind, &self.status))
.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))
.on_press_with_rectangle(move |offset, bounds| {
cosmic::Action::App(Message::OpenMenu(
@@ -365,6 +363,7 @@ impl FereseShell {
},
primary,
muted,
color_with_opacity(theme.accent, p),
p,
));
if connected {
@@ -408,6 +407,7 @@ impl FereseShell {
},
primary,
muted,
color_with_opacity(theme.accent, p),
p,
));
if b.enabled && !b.devices.is_empty() {
@@ -439,7 +439,14 @@ impl FereseShell {
]
.align_y(Alignment::Center),
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);
rows = rows.push(control_card(audio.into(), primary, p));
@@ -454,6 +461,7 @@ impl FereseShell {
value,
true,
primary,
color_with_opacity(theme.accent, p),
p,
)
]
@@ -502,7 +510,12 @@ impl FereseShell {
container(
row![
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),
)
@@ -639,9 +652,10 @@ fn status_summary<'a>(
subtitle: &'a str,
primary: Color,
muted: Color,
accent: Color,
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
let badge = container(symbolic_icon(source, 24))
let badge = container(accented_icon(source, 24, primary, accent))
.width(48)
.height(48)
.align_x(alignment::Horizontal::Center)
@@ -742,8 +756,9 @@ fn connection_control<'a>(
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
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 icon = container(symbolic_icon(source, 18))
let icon = container(accented_icon(source, 18, foreground, icon_accent))
.width(32)
.height(32)
.align_x(alignment::Horizontal::Center)
@@ -850,6 +865,7 @@ fn slider_row(
value: u8,
brightness: bool,
foreground: Color,
icon_accent: Color,
opacity: f32,
) -> Element<'static, cosmic::Action<Message>> {
let style = std::rc::Rc::new(move |_: &cosmic::Theme| {
@@ -900,7 +916,7 @@ fn slider_row(
dragging: style,
});
row![
symbolic_icon(source, 18),
accented_icon(source, 18, foreground, icon_accent),
control,
text(format!("{value}%")).size(12).width(36)
]
@@ -909,6 +925,36 @@ fn slider_row(
.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] {
if muted || volume == 0 {
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 {
Some(b) if !b.devices.is_empty() => (
Some(b) if b.enabled && !b.devices.is_empty() => (
include_bytes!("../assets/icons/status/bluetooth-connected.svg"),
true,
),
@@ -965,13 +1011,13 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
};
let icon: &'static [u8] = if b.status == "Charging" {
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")
} else if b.percent > 62 {
} else if b.percent >= 50 {
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")
} else if b.percent > 12 {
} else if b.percent > 0 {
include_bytes!("../assets/icons/status/battery-25.svg")
} else {
include_bytes!("../assets/icons/status/battery-empty.svg")
@@ -980,7 +1026,10 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
}
Menu::Notifications => {
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 {
(
include_bytes!("../assets/icons/status/notifications.svg"),
@@ -1042,5 +1091,62 @@ mod tests {
status_icon(Menu::Battery, &s).0,
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")
);
}
}
}