glass-base: a glass opacity setting and one place for the surface look

- shell.conf glass (0.05 to 1, default 1 = solid, as before): the
  surfaces' opacity; read live with the theme
- surface.rs: shell() draws the island, the idle pill and the workspace
  pill (fill, rim, shadow), so a design is one file; the default is the
  look so far
- see-through surfaces can't hide content under a veil, so with glass
  below 1 the open/close fades use .opacity() while they run
- README: the Hyprland layer rule that frosts what's behind the shell
  (namespace match is a full match: ^magic-.*$; ignore_alpha keeps the
  transparent margin around the island sharp)
- docs/design: the solid look and glass 0.5 with the rule, for review
This commit is contained in:
server2
2026-10-09 05:20:44 +02:00
parent 036b326b04
commit 54ca75b855
9 changed files with 90 additions and 41 deletions
+21 -27
View File
@@ -14,6 +14,7 @@ use crate::art::Art;
use crate::parse::{self, BANDS, Sink};
use crate::sound::{EQ_PRESETS, MAX_GAIN, Sound};
use crate::spectrum::Spectrum;
use crate::surface;
use crate::theme;
/*
@@ -331,26 +332,16 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
Rectangle::new().width(0.0).height(0.0)
};
let shape = Rectangle::new()
.width(width)
.height(height)
.radius(radius)
.fill(backdrop(&colors, 255))
.border(1.0, Color::blend(theme::now().outline, faded(theme::now().accent, 110), lift));
// the face centred along the top: while the shape grows it is narrower than the shape
let face = Rectangle::new().width(width).height(height).align_child(Center, Start).child(face);
/*
* one faint pink glow around the island, pill and card alike, stronger while the
* pill is hovered. blur 22 fades out within ~28 px (amane blurs with a deviation
* of blur / 2), inside the window's SHADOW margin. no clip anywhere: neither face
* ever overflows the shape
*/
let shape = shape.shadow(Shadow::drop(theme::now().accent).opacity(mix(0.3, 0.7, lift)).blur(22.0));
// the surface (fill, rim, glow) is surface.rs's; no clip anywhere: neither face ever overflows the shape
let shape = surface::shell(width, height, radius, &colors, lift, face);
let tapped = monitor.name.clone();
let hovered = monitor.name.clone();
let shape = shape
.align_child(Center, Start)
.on_click(move |_| tap(&tapped))
.on_hover(move |inside| {
if inside {
@@ -365,8 +356,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
if !inside && card_here {
collapse();
}
})
.child(face);
});
let shape = if island.expanded && opened_here { shape } else { shape.cursor(Pointer) };
@@ -551,13 +541,19 @@ fn phases(timeline: f32) -> (f32, f32, f32) {
* fades a face in over the island's backdrop by laying the same gradient over it,
* thinning as `shown` rises: the same picture as .opacity(shown), without a gpu layer.
* the veil spans the whole shape (minus the 1 px rim), so its gradient lines up with
* the backdrop's exactly; amane places gradients by each rectangle's own bounds
* the backdrop's exactly; amane places gradients by each rectangle's own bounds.
* a see-through surface can't hide anything under a veil, so glass fades with
* .opacity(), a gpu layer only while the fade runs
*/
fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, colors: &[Color; 3]) -> Rectangle {
if shown >= 1.0 {
return face;
}
if theme::glass() < 1.0 {
return Rectangle::new().width(width).height(height).align_child(Center, Start).child(face).opacity(shown);
}
let veil = Rectangle::new()
.width(width - 2.0)
.height(height - 2.0)
@@ -602,17 +598,15 @@ pub fn faded(color: Color, alpha: u8) -> Color {
Color::rgba(color.red(), color.green(), color.blue(), alpha)
}
// the island while no player is open: a music note and "Nothing playing", glowing like the pill
// the island while no player is open: a music note and "Nothing playing"
fn idle(colors: &[Color; 3]) -> Rectangle {
Rectangle::new()
.width(COMPACT_W)
.height(COMPACT_H)
.radius(Full)
.fill(backdrop(colors, 255))
.border(1.0, theme::now().outline)
.shadow(Shadow::drop(theme::now().accent).opacity(0.3).blur(22.0))
.align_child(Center, Center)
.child(Row::new(children![icon(Glyph::Note, 15.0, theme::now().accent), Text::new("Nothing playing").size(12.0).color(theme::now().dim)]).gap(8.0).align(Center))
let content = Rectangle::new().width(COMPACT_W).height(COMPACT_H).align_child(Center, Center).child(
Row::new(children![icon(Glyph::Note, 15.0, theme::now().accent), Text::new("Nothing playing").size(12.0).color(theme::now().dim)])
.gap(8.0)
.align(Center),
);
surface::shell(COMPACT_W, COMPACT_H, COMPACT_H / 2.0, colors, 0.0, content)
}
// the pill: cover, title, the player's live levels
+1
View File
@@ -3,6 +3,7 @@ mod island;
mod parse;
mod sound;
mod spectrum;
mod surface;
mod theme;
mod workspaces;
+28
View File
@@ -0,0 +1,28 @@
use amane::{Blend, Color, Rectangle, Shadow};
use crate::island::{backdrop, faded};
use crate::theme;
/*
* what the island, the idle pill and the workspace pill are made of: one place, so
* a design is this file. `shell` wraps `content` (already width x height, with its
* own alignment and padding) in the surface. `lift` (0 to 1) is how hovered it is.
* the default: the gradient of `colors`, a 1 px rim, a soft glow in the accent
*/
pub fn shell(width: f32, height: f32, radius: f32, colors: &[Color; 3], lift: f32, content: Rectangle) -> Rectangle {
let theme = theme::now();
Rectangle::new()
.width(width)
.height(height)
.radius(radius)
.fill(backdrop(colors, alpha(theme::glass())))
.border(1.0, Color::blend(theme.outline, faded(theme.accent, 110), lift))
.shadow(Shadow::drop(theme.accent).opacity(0.3 + 0.4 * lift).blur(22.0))
.child(content)
}
// an opacity, 0 to 1, as a colour's alpha
pub fn alpha(opacity: f32) -> u8 {
(opacity.clamp(0.0, 1.0) * 255.0).round() as u8
}
+13 -1
View File
@@ -73,6 +73,9 @@ pub struct Theme {
// the open media card and the equalizer take the cover's colours as their background
pub cover_colors: bool,
// how opaque the shell's surfaces are, 0.05 to 1; below 1 the desktop shows through
glass: f32,
}
impl Service for Theme {
@@ -80,6 +83,7 @@ impl Service for Theme {
Self {
colors: Animation::new(PINK_ON_PURPLE),
cover_colors: false,
glass: 1.0,
}
}
@@ -174,6 +178,7 @@ fn apply(theme: &str, config: &str, startup: bool) {
let mut theme = Theme::write();
theme.cover_colors = setting("cover_colors") == Some("true");
theme.glass = setting("glass").and_then(|glass| glass.parse::<f32>().ok()).map_or(1.0, |glass| glass.clamp(0.05, 1.0));
// a change in the middle of a fade starts from the colours on screen now
let now = theme.colors.value();
@@ -182,9 +187,10 @@ fn apply(theme: &str, config: &str, startup: bool) {
theme.colors.to(colors);
}
// the colours of the frame being drawn, set by the view that draws it
// the colours and glass of the frame being drawn, set by the view that draws it
thread_local! {
static NOW: Cell<Colors> = const { Cell::new(PINK_ON_PURPLE) };
static GLASS: Cell<f32> = const { Cell::new(1.0) };
}
/*
@@ -195,6 +201,7 @@ pub fn begin() -> bool {
let theme = Theme::read();
NOW.set(theme.colors.value());
GLASS.set(theme.glass);
theme.cover_colors
}
@@ -202,3 +209,8 @@ pub fn begin() -> bool {
pub fn now() -> Colors {
NOW.get()
}
// the surfaces' opacity this frame, 0.05 to 1
pub fn glass() -> f32 {
GLASS.get()
}
+5 -13
View File
@@ -5,10 +5,11 @@ use std::time::Duration;
use amane::{
Animation, Blend, Center, Color, Full, Horizontal, InputArea, Layer, LayerWindow, Monitor, Padding, Parent, Pointer,
Rectangle, Row, Service, Shadow, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone,
Rectangle, Row, Service, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone,
};
use crate::island::{CHIP_HOVER, CLEAR, COMPACT_H, HOVER_MS, SHADOW, TOP_GAP, backdrop, faded};
use crate::island::{CHIP_HOVER, CLEAR, COMPACT_H, HOVER_MS, SHADOW, TOP_GAP, faded};
use crate::surface;
use crate::theme;
// one round slot per workspace, inside the pill's inset; the pill sits LEFT px from the monitor's edge
@@ -107,18 +108,9 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
));
}
let theme = theme::now();
let content = Rectangle::new().width(width).height(COMPACT_H).padding(INSET).align_child(Start, Center).child(Row::new(slots).gap(SLOT_GAP).align(Center));
let pill = Rectangle::new()
.width(width)
.height(COMPACT_H)
.radius(Full)
.fill(backdrop(&theme.surfaces(), 255))
.border(1.0, theme.outline)
.shadow(Shadow::drop(theme.accent).opacity(0.3).blur(22.0))
.padding(INSET)
.align_child(Start, Center)
.child(Row::new(slots).gap(SLOT_GAP).align(Center));
let pill = surface::shell(width, COMPACT_H, COMPACT_H / 2.0, &theme::now().surfaces(), 0.0, content);
// clicks next to the pill reach the windows underneath
let region = InputArea {