Glass designs as settings: solid, frosted, liquid on every surface
appearance.surface.design picks the look (chips in the config window); each design's opacity, rim and light are settings. surface::shell now draws the island, idle pill, workspace and tray pills, tray menu, control centre and popup, so a design applies everywhere. On see-through glass faces fade with .opacity(). Shell text settings can list their allowed words (chips).
This commit is contained in:
@@ -117,6 +117,23 @@ Every setting lives in `~/.config/amane/config.toml`: sizes, timings, text sizes
|
|||||||
|
|
||||||
The config window edits the same file: Settings at the foot of the control centre, or `amane ipc call config toggle`. It is a normal window with a page per part of the shell, a page per Hyprland category, and a search over everything. Numbers with a range get a slider and a field, switches toggle, colours show a swatch; a typed value applies on Enter. Reset brings back the default.
|
The config window edits the same file: Settings at the foot of the control centre, or `amane ipc call config toggle`. It is a normal window with a page per part of the shell, a page per Hyprland category, and a search over everything. Numbers with a range get a slider and a field, switches toggle, colours show a swatch; a typed value applies on Enter. Reset brings back the default.
|
||||||
|
|
||||||
|
### Glass
|
||||||
|
|
||||||
|
`appearance.surface.design` picks what the island, the pills, the tray menu, the control centre and its popups are made of: `solid` (the wallpaper's gradient with an accent glow), `frosted` (a thin tint, a hairline white rim, a sheen along the top, a soft shadow) or `liquid` (clear glass, denser toward the bottom right, a light-catching edge, the accent's glow). Each design's opacity, rim and light are settings of their own (`appearance.solid|frosted|liquid.*`); the look itself is `src/surface.rs`. On see-through glass a face fades with `.opacity()` instead of a veil, a GPU layer only while it fades.
|
||||||
|
|
||||||
|
Amane can only blur what the shell draws, so the frosting comes from Hyprland: a layer rule for the shell's windows (the namespace match is a full match). `ignore_alpha` leaves pixels at or below that opacity unblurred, which keeps the clear margin around the pills sharp; keep it below the design's thinnest part, or that part shows the desktop unblurred (liquid at its default 0.4 is 0.22 in its top left corner).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
hl.layer_rule({
|
||||||
|
name = "amane-glass",
|
||||||
|
match = { namespace = "^magic-.*$" },
|
||||||
|
blur = true,
|
||||||
|
ignore_alpha = 0.2,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The blur's strength is Hyprland's own (`decoration.blur.size`, `passes`; also on the config window's Decoration page).
|
||||||
|
|
||||||
### Hyprland options
|
### Hyprland options
|
||||||
|
|
||||||
The Hyprland pages list every option Hyprland itself describes (`hyprctl descriptions`), with its current value and default, and whether your Hyprland config sets it. A change applies at once (`hl.config`) and is kept in `config.toml` under `[hyprland.*]`; the shell writes those to `~/.config/hypr/amane.lua`. Load that file last in `hyprland.lua`, so the window's values survive a Hyprland reload and win over the rest of your config:
|
The Hyprland pages list every option Hyprland itself describes (`hyprctl descriptions`), with its current value and default, and whether your Hyprland config sets it. A change applies at once (`hl.config`) and is kept in `config.toml` under `[hyprland.*]`; the shell writes those to `~/.config/hypr/amane.lua`. Load that file last in `hyprland.lua`, so the window's values survive a Hyprland reload and win over the rest of your config:
|
||||||
@@ -147,6 +164,7 @@ src/png.rs a stdlib png writer, for tray icons sent as pixels
|
|||||||
src/system.rs what the control centre shows: iwd, wired cards, openvpn, power, the external GPU
|
src/system.rs what the control centre shows: iwd, wired cards, openvpn, power, the external GPU
|
||||||
src/glyph.rs every drawn icon, as a Canvas or as shapes for a shared one
|
src/glyph.rs every drawn icon, as a Canvas or as shapes for a shared one
|
||||||
src/theme.rs the shell's colours: the matugen theme file, the cross-fade; watches config.toml
|
src/theme.rs the shell's colours: the matugen theme file, the cross-fade; watches config.toml
|
||||||
|
src/surface.rs what the island, pills, menus and popups are made of: the solid, frosted and liquid designs
|
||||||
src/config.rs every setting, declared once (typed accessors + the window's list), config.toml read and written
|
src/config.rs every setting, declared once (typed accessors + the window's list), config.toml read and written
|
||||||
src/hypr.rs Hyprland's options for the config window: hyprctl descriptions/getoption, hl.config, amane.lua
|
src/hypr.rs Hyprland's options for the config window: hyprctl descriptions/getoption, hl.config, amane.lua
|
||||||
src/settings.rs the config window, `config` IPC
|
src/settings.rs the config window, `config` IPC
|
||||||
|
|||||||
+29
-1
@@ -32,6 +32,8 @@ pub struct Opt {
|
|||||||
pub help: &'static str,
|
pub help: &'static str,
|
||||||
pub kind: Kind,
|
pub kind: Kind,
|
||||||
pub range: Option<(f64, f64)>,
|
pub range: Option<(f64, f64)>,
|
||||||
|
// the only words a text setting takes, shown as chips; empty: any text
|
||||||
|
pub choices: &'static [&'static str],
|
||||||
pub default: fn() -> Value,
|
pub default: fn() -> Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +172,7 @@ pub fn color(text: &str) -> Option<Color> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! settings {
|
macro_rules! settings {
|
||||||
($( $module:ident { $( $group:ident { $( $name:ident : $ty:ty = $default:expr $(, $min:literal ..= $max:literal)? ; $help:literal )* } )* } )*) => {
|
($( $module:ident { $( $group:ident { $( $name:ident : $ty:ty $([ $($choice:literal),+ ])? = $default:expr $(, $min:literal ..= $max:literal)? ; $help:literal )* } )* } )*) => {
|
||||||
$( pub mod $module {
|
$( pub mod $module {
|
||||||
$( pub mod $group {
|
$( pub mod $group {
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
@@ -190,6 +192,7 @@ macro_rules! settings {
|
|||||||
help: $help,
|
help: $help,
|
||||||
kind: <$ty as Setting>::KIND,
|
kind: <$ty as Setting>::KIND,
|
||||||
range: settings!(@range $($min, $max)?),
|
range: settings!(@range $($min, $max)?),
|
||||||
|
choices: &[ $( $($choice),+ )? ],
|
||||||
default: || <$ty as Setting>::value(&$default),
|
default: || <$ty as Setting>::value(&$default),
|
||||||
}, )* )* )* ];
|
}, )* )* )* ];
|
||||||
};
|
};
|
||||||
@@ -221,6 +224,27 @@ settings! {
|
|||||||
motion {
|
motion {
|
||||||
hover_ms: u64 = 220, 0..=2000; "How long hover highlights take to fade in and out"
|
hover_ms: u64 = 220, 0..=2000; "How long hover highlights take to fade in and out"
|
||||||
}
|
}
|
||||||
|
surface {
|
||||||
|
design: String ["solid", "frosted", "liquid"] = String::from("solid"); "What the island, the pills, the menus, the control centre and its popups are made of: solid (the wallpaper's gradient with an accent glow), frosted (a thin tint, a hairline rim and a sheen) or liquid (clear glass with a light-catching edge). See-through ones need Hyprland's blur behind the shell: README, Glass"
|
||||||
|
}
|
||||||
|
solid {
|
||||||
|
opacity: f32 = 1.0, 0.05..=1.0; "How opaque the solid design is; below 1 the desktop shows through"
|
||||||
|
glow: f32 = 0.3, 0.0..=1.0; "Strength of the accent glow around each surface"
|
||||||
|
hover_glow: f32 = 0.7, 0.0..=1.0; "Strength of the glow while hovered"
|
||||||
|
}
|
||||||
|
frosted {
|
||||||
|
opacity: f32 = 0.62, 0.05..=1.0; "How opaque the frosted tint is"
|
||||||
|
rim: f32 = 0.19, 0.0..=1.0; "Opacity of the hairline white rim"
|
||||||
|
sheen: f32 = 0.13, 0.0..=1.0; "Opacity of the white highlight along the top edge"
|
||||||
|
shadow: f32 = 0.24, 0.0..=1.0; "Strength of the soft shadow under each surface"
|
||||||
|
}
|
||||||
|
liquid {
|
||||||
|
opacity: f32 = 0.4, 0.05..=1.0; "How opaque the glass is in its middle; clearer at the top left, denser at the bottom right"
|
||||||
|
rim: f32 = 0.33, 0.0..=1.0; "Opacity of the light rim"
|
||||||
|
rim_width: f32 = 1.5, 0.0..=4.0; "Width of the light rim, in pixels"
|
||||||
|
light: f32 = 0.235, 0.0..=1.0; "Opacity of the light caught at the top left edge"
|
||||||
|
glow: f32 = 0.22, 0.0..=1.0; "Strength of the accent glow around each surface"
|
||||||
|
}
|
||||||
fallback {
|
fallback {
|
||||||
accent: Color = Color::rgb(247, 168, 200); "Accent, used until matugen writes ~/.config/amane/theme from the wallpaper"
|
accent: Color = Color::rgb(247, 168, 200); "Accent, used until matugen writes ~/.config/amane/theme from the wallpaper"
|
||||||
on_accent: Color = Color::rgb(40, 18, 32); "Text and icons on the accent colour, without a theme file"
|
on_accent: Color = Color::rgb(40, 18, 32); "Text and icons on the accent colour, without a theme file"
|
||||||
@@ -575,6 +599,10 @@ fn check(path: &str, value: &Value) -> Result<(), String> {
|
|||||||
return Err(format!("needs {}", kind_words(option)));
|
return Err(format!("needs {}", kind_words(option)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !option.choices.is_empty() && !value.text().is_some_and(|word| option.choices.contains(&word)) {
|
||||||
|
return Err(format!("must be one of {}", option.choices.join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
let numbers: Vec<f64> = match value {
|
let numbers: Vec<f64> = match value {
|
||||||
Value::List(items) => items.iter().filter_map(Value::number).collect(),
|
Value::List(items) => items.iter().filter_map(Value::number).collect(),
|
||||||
_ => value.number().into_iter().collect(),
|
_ => value.number().into_iter().collect(),
|
||||||
|
|||||||
+8
-13
@@ -11,7 +11,7 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use amane::{
|
use amane::{
|
||||||
Animation, Audio, Blend, Bluetooth, Brightness, Button, Canvas, Cap, Center, Color, Column, Easing, End, Full,
|
Animation, Audio, Blend, Bluetooth, Brightness, Button, Canvas, Cap, Center, Color, Column, Easing, End, Full,
|
||||||
Horizontal, InputArea, Layer, LayerWindow, Line, Monitor, Padding, Parent, Pointer, Rectangle, Row, Service, Shadow,
|
Horizontal, InputArea, Layer, LayerWindow, Line, Monitor, Padding, Parent, Pointer, Rectangle, Row, Service,
|
||||||
Shape, SpaceBetween, Stack, Start, Text, Vertical, Weight, Widget, Zone, children, shapes,
|
Shape, SpaceBetween, Stack, Start, Text, Vertical, Weight, Widget, Zone, children, shapes,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,14 +20,14 @@ use crate::config::control as c;
|
|||||||
use crate::config::island::{animation as island_animation, pill as island_pill};
|
use crate::config::island::{animation as island_animation, pill as island_pill};
|
||||||
use crate::edge::{self, Edge};
|
use crate::edge::{self, Edge};
|
||||||
use crate::glyph::{Glyph, icon};
|
use crate::glyph::{Glyph, icon};
|
||||||
use crate::island::{self, CLEAR, backdrop, faded, mix, phases, veiled};
|
use crate::island::{self, CLEAR, faded, mix, phases, veiled};
|
||||||
use crate::map::{self, Centre, Link, Node};
|
use crate::map::{self, Centre, Link, Node};
|
||||||
use crate::parse;
|
use crate::parse;
|
||||||
use crate::prompt;
|
use crate::prompt;
|
||||||
use crate::settings;
|
use crate::settings;
|
||||||
use crate::sound::Sound;
|
use crate::sound::Sound;
|
||||||
use crate::system::{self, Charge, Gpu, Links, Power, VpnState};
|
use crate::system::{self, Charge, Gpu, Links, Power, VpnState};
|
||||||
use crate::theme;
|
use crate::{surface, theme};
|
||||||
|
|
||||||
// every size and timing is a setting (config.rs: control.*); these follow from them
|
// every size and timing is a setting (config.rs: control.*); these follow from them
|
||||||
fn menu_w() -> f32 {
|
fn menu_w() -> f32 {
|
||||||
@@ -452,14 +452,10 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
|||||||
let tapped = monitor.name.clone();
|
let tapped = monitor.name.clone();
|
||||||
let hovered = monitor.name.clone();
|
let hovered = monitor.name.clone();
|
||||||
|
|
||||||
let shape = Rectangle::new()
|
let face = Rectangle::new().width(width).height(height).align_child(Center, Start).child(face);
|
||||||
.width(width)
|
|
||||||
.height(height)
|
// the surface is surface.rs's, like the island's
|
||||||
.radius(radius)
|
let shape = surface::shell(width, height, radius, &colors, lift, face)
|
||||||
.fill(backdrop(&colors, 255))
|
|
||||||
.border(1.0, Color::blend(theme::now().outline, faded(theme::now().accent, 110), lift))
|
|
||||||
.shadow(Shadow::drop(theme::now().accent).opacity(mix(0.3, 0.7, lift)).blur(22.0))
|
|
||||||
.align_child(Center, Start)
|
|
||||||
.on_click(move |_| tap(&tapped))
|
.on_click(move |_| tap(&tapped))
|
||||||
.on_hover(move |inside| {
|
.on_hover(move |inside| {
|
||||||
{
|
{
|
||||||
@@ -481,8 +477,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
|||||||
if !inside && panel_here {
|
if !inside && panel_here {
|
||||||
collapse_soon();
|
collapse_soon();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.child(face);
|
|
||||||
|
|
||||||
let shape = if control.expanded && opened_here { shape } else { shape.cursor(Pointer) };
|
let shape = if control.expanded && opened_here { shape } else { shape.cursor(Pointer) };
|
||||||
|
|
||||||
|
|||||||
+20
-23
@@ -16,7 +16,7 @@ use crate::glyph::{Glyph, bold_icon, icon};
|
|||||||
use crate::parse::{self, Sink};
|
use crate::parse::{self, Sink};
|
||||||
use crate::sound::{self, Sound};
|
use crate::sound::{self, Sound};
|
||||||
use crate::spectrum::Spectrum;
|
use crate::spectrum::Spectrum;
|
||||||
use crate::theme;
|
use crate::{surface, theme};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* opening is one linear timeline in two phases, so the shape never resizes with the
|
* opening is one linear timeline in two phases, so the shape never resizes with the
|
||||||
@@ -317,26 +317,20 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
|||||||
Rectangle::new().width(0.0).height(0.0)
|
Rectangle::new().width(0.0).height(0.0)
|
||||||
};
|
};
|
||||||
|
|
||||||
let shape = Rectangle::new()
|
// the face centred along the top: while the shape grows it is narrower than the shape
|
||||||
.width(width)
|
let face = Rectangle::new().width(width).height(height).align_child(Center, Start).child(face);
|
||||||
.height(height)
|
|
||||||
.radius(radius)
|
|
||||||
.fill(backdrop(&colors, 255))
|
|
||||||
.border(1.0, Color::blend(theme::now().outline, faded(theme::now().accent, 110), lift));
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* one faint pink glow around the island, pill and card alike, stronger while the
|
* the surface (fill, rim, glow; stronger while hovered) is surface.rs's. its glow fades
|
||||||
* pill is hovered. blur 22 fades out within ~28 px (amane blurs with a deviation
|
* out inside the window's c::pill::shadow() margin. no clip anywhere: neither face ever
|
||||||
* of blur / 2), inside the window's c::pill::shadow() margin. no clip anywhere: neither face
|
* overflows the shape
|
||||||
* ever overflows the shape
|
|
||||||
*/
|
*/
|
||||||
let shape = shape.shadow(Shadow::drop(theme::now().accent).opacity(mix(0.3, 0.7, lift)).blur(22.0));
|
let shape = surface::shell(width, height, radius, &colors, lift, face);
|
||||||
|
|
||||||
let tapped = monitor.name.clone();
|
let tapped = monitor.name.clone();
|
||||||
let hovered = monitor.name.clone();
|
let hovered = monitor.name.clone();
|
||||||
|
|
||||||
let shape = shape
|
let shape = shape
|
||||||
.align_child(Center, Start)
|
|
||||||
.on_click(move |_| tap(&tapped))
|
.on_click(move |_| tap(&tapped))
|
||||||
.on_hover(move |inside| {
|
.on_hover(move |inside| {
|
||||||
if inside {
|
if inside {
|
||||||
@@ -352,8 +346,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
|||||||
if !inside && card_here {
|
if !inside && card_here {
|
||||||
collapse();
|
collapse();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.child(face);
|
|
||||||
|
|
||||||
let shape = if island.expanded && opened_here { shape } else { shape.cursor(Pointer) };
|
let shape = if island.expanded && opened_here { shape } else { shape.cursor(Pointer) };
|
||||||
|
|
||||||
@@ -539,13 +532,19 @@ pub fn phases(timeline: f32) -> (f32, f32, f32) {
|
|||||||
* fades a face in over the island's backdrop by laying the same gradient over it,
|
* 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.
|
* 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 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.
|
||||||
|
* see-through glass can't hide anything under a veil, so there faces fade with
|
||||||
|
* .opacity(), a gpu layer only while the fade runs
|
||||||
*/
|
*/
|
||||||
pub fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, colors: &[Color; 3]) -> Rectangle {
|
pub fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, colors: &[Color; 3]) -> Rectangle {
|
||||||
if shown >= 1.0 {
|
if shown >= 1.0 {
|
||||||
return face;
|
return face;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if surface::clear() {
|
||||||
|
return Rectangle::new().width(width).height(height).align_child(Center, Start).child(face).opacity(shown);
|
||||||
|
}
|
||||||
|
|
||||||
let veil = Rectangle::new()
|
let veil = Rectangle::new()
|
||||||
.width(width - 2.0)
|
.width(width - 2.0)
|
||||||
.height(height - 2.0)
|
.height(height - 2.0)
|
||||||
@@ -590,17 +589,15 @@ pub fn faded(color: Color, alpha: u8) -> Color {
|
|||||||
Color::rgba(color.red(), color.green(), color.blue(), alpha)
|
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", on the pill's surface
|
||||||
fn idle(colors: &[Color; 3]) -> Rectangle {
|
fn idle(colors: &[Color; 3]) -> Rectangle {
|
||||||
Rectangle::new()
|
let content = Rectangle::new()
|
||||||
.width(c::pill::width())
|
.width(c::pill::width())
|
||||||
.height(c::pill::height())
|
.height(c::pill::height())
|
||||||
.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)
|
.align_child(Center, Center)
|
||||||
.child(Row::new(children![icon(Glyph::Note, 15.0, theme::now().accent), Text::new("Nothing playing").size(text::body()).color(theme::now().dim)]).gap(8.0).align(Center))
|
.child(Row::new(children![icon(Glyph::Note, 15.0, theme::now().accent), Text::new("Nothing playing").size(text::body()).color(theme::now().dim)]).gap(8.0).align(Center));
|
||||||
|
|
||||||
|
surface::shell(c::pill::width(), c::pill::height(), c::pill::height() / 2.0, colors, 0.0, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the pill: cover, title, the player's live levels
|
// the pill: cover, title, the player's live levels
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod prompt;
|
|||||||
mod settings;
|
mod settings;
|
||||||
mod sound;
|
mod sound;
|
||||||
mod spectrum;
|
mod spectrum;
|
||||||
|
mod surface;
|
||||||
mod system;
|
mod system;
|
||||||
mod png;
|
mod png;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|||||||
+5
-12
@@ -14,7 +14,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use amane::{
|
use amane::{
|
||||||
Animation, Blend, Center, Color, Column, End, Full, Horizontal, InputArea, Key, Keyboard, Layer, LayerWindow, Margin,
|
Animation, Blend, Center, Color, Column, End, Full, Horizontal, InputArea, Key, Keyboard, Layer, LayerWindow, Margin,
|
||||||
Monitor, Padding, Parent, Pointer, Rectangle, Row, Service, Shadow, Start, Text, TextInput, Vertical, Weight, Widget,
|
Monitor, Padding, Parent, Pointer, Rectangle, Row, Service, Start, Text, TextInput, Vertical, Weight, Widget,
|
||||||
Zone, children,
|
Zone, children,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,10 +24,10 @@ use crate::config::island::pill as island_pill;
|
|||||||
use crate::config::popups::{layout, timing};
|
use crate::config::popups::{layout, timing};
|
||||||
use crate::control::{self, Control};
|
use crate::control::{self, Control};
|
||||||
use crate::glyph::{Glyph, icon};
|
use crate::glyph::{Glyph, icon};
|
||||||
use crate::island::{self, backdrop, veiled};
|
use crate::island::{self, veiled};
|
||||||
use crate::parse::{self, Pairing};
|
use crate::parse::{self, Pairing};
|
||||||
use crate::system;
|
use crate::system;
|
||||||
use crate::theme;
|
use crate::{surface, theme};
|
||||||
|
|
||||||
// the popup's sizes are settings (config.rs: popups.*)
|
// the popup's sizes are settings (config.rs: popups.*)
|
||||||
fn inner_w() -> f32 {
|
fn inner_w() -> f32 {
|
||||||
@@ -621,15 +621,8 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
|||||||
|
|
||||||
let content = Rectangle::new().width(layout::width()).height(card_h).padding(layout::pad()).child(Column::new(rows).gap(layout::gap()));
|
let content = Rectangle::new().width(layout::width()).height(card_h).padding(layout::pad()).child(Column::new(rows).gap(layout::gap()));
|
||||||
|
|
||||||
let card = Rectangle::new()
|
// raised a little over the control centre: a stronger glow and rim than a resting surface
|
||||||
.width(layout::width())
|
let card = surface::shell(layout::width(), card_h, 22.0, &colors, 0.375, veiled(content, layout::width(), card_h, 22.0, appear, &colors)).translate(0.0, 8.0 * (1.0 - appear));
|
||||||
.height(card_h)
|
|
||||||
.radius(22.0)
|
|
||||||
.fill(backdrop(&colors, 255))
|
|
||||||
.border(1.0, Color::blend(theme.outline, theme.accent, 0.5))
|
|
||||||
.shadow(Shadow::drop(theme.accent).opacity(0.45).blur(22.0))
|
|
||||||
.translate(0.0, 8.0 * (1.0 - appear))
|
|
||||||
.child(veiled(content, layout::width(), card_h, 22.0, appear, &colors));
|
|
||||||
|
|
||||||
// over the middle of the map menu, a little below the control centre's top
|
// over the middle of the map menu, a little below the control centre's top
|
||||||
let (right, top) = (control_pill::right() + (control::map_width(monitor.width as f32) - layout::width()) / 2.0 - island_pill::shadow(), island_pill::top_gap() + 110.0 - island_pill::shadow());
|
let (right, top) = (control_pill::right() + (control::map_width(monitor.width as f32) - layout::width()) / 2.0 - island_pill::shadow(), island_pill::top_gap() + 110.0 - island_pill::shadow());
|
||||||
|
|||||||
+33
-10
@@ -480,6 +480,14 @@ fn shell_row(option: &'static Opt, width: f32, colors: &Colors) -> Rectangle {
|
|||||||
|
|
||||||
Box::new(switch(on, move || say(config::set(option.path, Value::Bool(!on)), format!("{} = {}", option.path, !on))))
|
Box::new(switch(on, move || say(config::set(option.path, Value::Bool(!on)), format!("{} = {}", option.path, !on))))
|
||||||
}
|
}
|
||||||
|
(Kind::Text, Value::Text(current)) if !option.choices.is_empty() => Box::new(chips(
|
||||||
|
option
|
||||||
|
.choices
|
||||||
|
.iter()
|
||||||
|
.map(|&word| (String::from(word), current == word, Box::new(move || say(config::set(option.path, Value::Text(String::from(word))), format!("{} = {word}", option.path))) as Box<dyn Fn()>))
|
||||||
|
.collect(),
|
||||||
|
colors,
|
||||||
|
)),
|
||||||
(Kind::Int | Kind::Float, _) if option.range.is_some() => {
|
(Kind::Int | Kind::Float, _) if option.range.is_some() => {
|
||||||
let (min, max) = option.range.unwrap_or((0.0, 1.0));
|
let (min, max) = option.range.unwrap_or((0.0, 1.0));
|
||||||
let whole = option.kind == Kind::Int;
|
let whole = option.kind == Kind::Int;
|
||||||
@@ -514,8 +522,6 @@ fn shell_row(option: &'static Opt, width: f32, colors: &Colors) -> Rectangle {
|
|||||||
help.push_str(&format!(" · default {}", config::shown(&(option.default)())));
|
help.push_str(&format!(" · default {}", config::shown(&(option.default)())));
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = colors;
|
|
||||||
|
|
||||||
row(config::label(option.name()), option.path.to_string(), help, control, reset, width)
|
row(config::label(option.name()), option.path.to_string(), help, control, reset, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,14 +639,31 @@ fn slider(fraction: f64, pick: impl Fn(f64) + Clone + 'static) -> Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// a Hyprland number with named values: one chip per name
|
// a Hyprland number with named values: one chip per name
|
||||||
|
// a Hyprland option's named values as chips
|
||||||
fn choices(option: &HyprOption, colors: &Colors) -> Row {
|
fn choices(option: &HyprOption, colors: &Colors) -> Row {
|
||||||
let name = option.name;
|
let name = option.name;
|
||||||
let chips: Vec<Box<dyn Widget>> = option
|
|
||||||
.choices
|
chips(
|
||||||
.iter()
|
option
|
||||||
.map(|(label, value)| {
|
.choices
|
||||||
let chosen = option.current == value.to_string();
|
.iter()
|
||||||
let value = value.to_string();
|
.map(|(label, value)| {
|
||||||
|
let value = value.to_string();
|
||||||
|
|
||||||
|
(label.replace('_', " "), option.current == value, Box::new(move || hypr::set(name, &value)) as Box<dyn Fn()>)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
colors,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// one choice: its label, whether it's the current one, what a click does
|
||||||
|
type Choice = (String, bool, Box<dyn Fn()>);
|
||||||
|
|
||||||
|
fn chips(items: Vec<Choice>, colors: &Colors) -> Row {
|
||||||
|
let chips: Vec<Box<dyn Widget>> = items
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, chosen, pick)| {
|
||||||
let letters = label.chars().count() as f32;
|
let letters = label.chars().count() as f32;
|
||||||
|
|
||||||
Box::new(
|
Box::new(
|
||||||
@@ -651,8 +674,8 @@ fn choices(option: &HyprOption, colors: &Colors) -> Row {
|
|||||||
.fill(if chosen { colors.accent } else { config::appearance::overlays::chip() })
|
.fill(if chosen { colors.accent } else { config::appearance::overlays::chip() })
|
||||||
.align_child(Center, Center)
|
.align_child(Center, Center)
|
||||||
.cursor(Cursor::Pointer)
|
.cursor(Cursor::Pointer)
|
||||||
.on_click(move |_| hypr::set(name, &value))
|
.on_click(move |_| pick())
|
||||||
.child(Text::new(label.replace('_', " ")).size(config::appearance::text::small()).color(if chosen { colors.on_accent } else { colors.text })),
|
.child(Text::new(label).size(config::appearance::text::small()).color(if chosen { colors.on_accent } else { colors.text })),
|
||||||
) as Box<dyn Widget>
|
) as Box<dyn Widget>
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use amane::{Blend, Color, Gradient, Rectangle, Shadow, Stack, children};
|
||||||
|
|
||||||
|
use crate::config::appearance::{frosted, liquid, solid, surface};
|
||||||
|
use crate::island::{backdrop, faded};
|
||||||
|
use crate::theme;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* what the island, its idle pill, the workspace and tray pills, the tray menu, the control
|
||||||
|
* centre and its popup are made of: one place, so a design is this file. `shell` wraps
|
||||||
|
* `content` (already width x height, with its own padding and alignment) in the surface.
|
||||||
|
* `lift` (0 to 1) is how raised it is: hovered, or a popup over the rest.
|
||||||
|
* see-through designs need Hyprland's blur behind the shell (README: Glass)
|
||||||
|
*/
|
||||||
|
pub fn shell(width: f32, height: f32, radius: f32, colors: &[Color; 3], lift: f32, content: Rectangle) -> Rectangle {
|
||||||
|
let theme = theme::now();
|
||||||
|
let base = Rectangle::new().width(width).height(height).radius(radius);
|
||||||
|
|
||||||
|
match surface::design().as_str() {
|
||||||
|
// after Apple's materials: a thin tint, a hairline light rim (the accent when hovered),
|
||||||
|
// a soft highlight along the top as if lit from above, one soft low shadow instead of a glow
|
||||||
|
"frosted" => {
|
||||||
|
let sheen = inner(width, height, radius).fill(Gradient::linear(180.0, [(0.0, white(frosted::sheen())), (0.3, white(frosted::sheen() * 0.26)), (0.65, white(0.0))]));
|
||||||
|
|
||||||
|
base.fill(backdrop(colors, alpha(frosted::opacity())))
|
||||||
|
.border(1.0, Color::blend(white(frosted::rim()), faded(theme.accent, 150), lift))
|
||||||
|
.shadow(Shadow::drop(Color::BLACK).opacity(frosted::shadow() + 0.1 * lift).blur(24.0).offset(0.0, 6.0))
|
||||||
|
.child(Stack::new(children![sheen, content]).width(width).height(height))
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear glass, denser toward the bottom right where the equalizer's labels sit; a bright
|
||||||
|
// edge where light enters at the top left and leaves at the bottom right; the accent's glow
|
||||||
|
"liquid" => {
|
||||||
|
let glass = liquid::opacity();
|
||||||
|
let fill = Gradient::linear(150.0, [(0.0, faded(colors[0], alpha(glass * 0.55))), (0.55, faded(colors[1], alpha(glass))), (1.0, faded(colors[2], alpha(glass * 1.6)))]);
|
||||||
|
|
||||||
|
let light = liquid::light();
|
||||||
|
let edge = inner(width, height, radius).fill(Gradient::linear(
|
||||||
|
135.0,
|
||||||
|
[(0.0, white(light)), (0.16, white(light * 0.23)), (0.4, white(0.0)), (0.82, white(0.0)), (1.0, white(light * 0.43))],
|
||||||
|
));
|
||||||
|
|
||||||
|
base.fill(fill)
|
||||||
|
.border(liquid::rim_width(), Color::blend(white(liquid::rim()), faded(theme.flame, 200), lift))
|
||||||
|
.shadow(Shadow::drop(theme.accent).opacity(liquid::glow() + 0.3 * lift).blur(24.0))
|
||||||
|
.child(Stack::new(children![edge, content]).width(width).height(height))
|
||||||
|
}
|
||||||
|
|
||||||
|
// the original: the wallpaper's gradient, a 1 px rim, a soft glow in the accent
|
||||||
|
_ => base
|
||||||
|
.fill(backdrop(colors, alpha(solid::opacity())))
|
||||||
|
.border(1.0, Color::blend(theme.outline, faded(theme.accent, 110), lift))
|
||||||
|
.shadow(Shadow::drop(theme.accent).opacity(solid::glow() + (solid::hover_glow() - solid::glow()) * lift).blur(22.0))
|
||||||
|
.child(content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// whether the desktop shows through: then a veil can't hide a face, so faces fade with .opacity()
|
||||||
|
pub fn clear() -> bool {
|
||||||
|
surface::design() != "solid" || solid::opacity() < 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// a layer just inside the rim, so the rim stays crisp over it
|
||||||
|
fn inner(width: f32, height: f32, radius: f32) -> Rectangle {
|
||||||
|
Rectangle::new().width(width - 2.0).height(height - 2.0).radius((radius - 1.0).max(0.0)).translate(1.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// an opacity, 0 to 1, as a colour's alpha
|
||||||
|
fn alpha(opacity: f32) -> u8 {
|
||||||
|
(opacity.clamp(0.0, 1.0) * 255.0).round() as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
fn white(opacity: f32) -> Color {
|
||||||
|
Color::rgba(255, 255, 255, alpha(opacity))
|
||||||
|
}
|
||||||
+9
-23
@@ -16,7 +16,7 @@ use std::{env, fs, thread};
|
|||||||
|
|
||||||
use amane::{
|
use amane::{
|
||||||
Animation, Argument, Blend, Bus, Button, Center, Color, Column, Full, Horizontal, Image, InputArea, Layer,
|
Animation, Argument, Blend, Bus, Button, Center, Color, Column, Full, Horizontal, Image, InputArea, Layer,
|
||||||
LayerWindow, Margin, Monitor, Padding, Parent, Pointer, Rectangle, Row, Scroll, Service, Shadow, SpaceBetween,
|
LayerWindow, Margin, Monitor, Padding, Parent, Pointer, Rectangle, Row, Scroll, Service, SpaceBetween,
|
||||||
Start, Text, Value, Vertical, Weight, Widget, Zone,
|
Start, Text, Value, Vertical, Weight, Widget, Zone,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@ use crate::config::appearance::{motion, overlays, text};
|
|||||||
use crate::config::island::pill as island_pill;
|
use crate::config::island::pill as island_pill;
|
||||||
use crate::config::tray::{menu, pill};
|
use crate::config::tray::{menu, pill};
|
||||||
use crate::glyph::{Glyph, icon};
|
use crate::glyph::{Glyph, icon};
|
||||||
use crate::island::{CLEAR, backdrop, faded, veiled};
|
use crate::island::{CLEAR, faded, veiled};
|
||||||
use crate::{edge, parse, png, theme};
|
use crate::{edge, parse, png, surface, theme};
|
||||||
|
|
||||||
const WATCHER: &str = "org.kde.StatusNotifierWatcher";
|
const WATCHER: &str = "org.kde.StatusNotifierWatcher";
|
||||||
const WATCHER_PATH: &str = "/StatusNotifierWatcher";
|
const WATCHER_PATH: &str = "/StatusNotifierWatcher";
|
||||||
@@ -702,17 +702,9 @@ pub fn pill(monitor: &str, left: f32) -> Option<(Rectangle, f32)> {
|
|||||||
slots.push(Box::new(slot));
|
slots.push(Box::new(slot));
|
||||||
}
|
}
|
||||||
|
|
||||||
let pill = Rectangle::new()
|
let content = Rectangle::new().width(width).height(island_pill::height()).padding(pill::inset()).align_child(Start, Center).child(Row::new(slots).gap(pill::slot_gap()).align(Center));
|
||||||
.width(width)
|
|
||||||
.height(island_pill::height())
|
let pill = surface::shell(width, island_pill::height(), island_pill::height() / 2.0, &theme.surfaces(), 0.0, content).on_hover(edge::over_bar);
|
||||||
.radius(Full)
|
|
||||||
.fill(backdrop(&theme.surfaces(), 255))
|
|
||||||
.border(1.0, theme.outline)
|
|
||||||
.shadow(Shadow::drop(theme.accent).opacity(0.3).blur(22.0))
|
|
||||||
.padding(pill::inset())
|
|
||||||
.align_child(Start, Center)
|
|
||||||
.on_hover(edge::over_bar)
|
|
||||||
.child(Row::new(slots).gap(pill::slot_gap()).align(Center));
|
|
||||||
|
|
||||||
Some((pill, width))
|
Some((pill, width))
|
||||||
}
|
}
|
||||||
@@ -822,13 +814,8 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow {
|
|||||||
|
|
||||||
let content = Rectangle::new().width(menu::width()).height(height).padding(menu::pad()).child(Column::new(rows));
|
let content = Rectangle::new().width(menu::width()).height(height).padding(menu::pad()).child(Column::new(rows));
|
||||||
|
|
||||||
let card = Rectangle::new()
|
// raised a little over the bar
|
||||||
.width(menu::width())
|
let card = surface::shell(menu::width(), height, 16.0, &colors, 0.125, veiled(content, menu::width(), height, 16.0, appear, &colors))
|
||||||
.height(height)
|
|
||||||
.radius(16.0)
|
|
||||||
.fill(backdrop(&colors, 255))
|
|
||||||
.border(1.0, theme.outline)
|
|
||||||
.shadow(Shadow::drop(theme.accent).opacity(0.35).blur(22.0))
|
|
||||||
.translate(0.0, -6.0 * (1.0 - appear))
|
.translate(0.0, -6.0 * (1.0 - appear))
|
||||||
.on_hover(|inside| {
|
.on_hover(|inside| {
|
||||||
if inside {
|
if inside {
|
||||||
@@ -839,8 +826,7 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow {
|
|||||||
} else {
|
} else {
|
||||||
leave_soon();
|
leave_soon();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.child(veiled(content, menu::width(), height, 16.0, appear, &colors));
|
|
||||||
|
|
||||||
let left = (open.x - 8.0 - island_pill::shadow()).max(0.0);
|
let left = (open.x - 8.0 - island_pill::shadow()).max(0.0);
|
||||||
let top = island_pill::top_gap() + island_pill::height() + 6.0 - island_pill::shadow();
|
let top = island_pill::top_gap() + island_pill::height() + 6.0 - island_pill::shadow();
|
||||||
|
|||||||
+6
-14
@@ -5,15 +5,15 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use amane::{
|
use amane::{
|
||||||
Animation, Blend, Center, Color, Full, Horizontal, InputArea, Layer, LayerWindow, Monitor, Padding, Parent, Pointer,
|
Animation, Blend, Center, Color, Full, Horizontal, InputArea, Layer, LayerWindow, Monitor, Padding, Parent, Pointer,
|
||||||
Rectangle, Row, Service, Shadow, Stack, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone,
|
Rectangle, Row, Service, Stack, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::appearance::{motion, overlays, text};
|
use crate::config::appearance::{motion, overlays, text};
|
||||||
use crate::config::island::pill as island_pill;
|
use crate::config::island::pill as island_pill;
|
||||||
use crate::config::workspaces::pill;
|
use crate::config::workspaces::pill;
|
||||||
use crate::edge::{self, Edge, Part};
|
use crate::edge::{self, Edge, Part};
|
||||||
use crate::island::{CLEAR, backdrop, faded, mix};
|
use crate::island::{CLEAR, faded, mix};
|
||||||
use crate::{theme, tray};
|
use crate::{surface, theme, tray};
|
||||||
|
|
||||||
// one round slot per workspace, inside the pill's inset, the pill pill::left() px from the monitor's edge,
|
// one round slot per workspace, inside the pill's inset, the pill pill::left() px from the monitor's edge,
|
||||||
// the tray pill::tray_gap() after it (config.rs: workspaces.*)
|
// the tray pill::tray_gap() after it (config.rs: workspaces.*)
|
||||||
@@ -196,17 +196,9 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
|||||||
|
|
||||||
layers.extend(slots);
|
layers.extend(slots);
|
||||||
|
|
||||||
let pill = Rectangle::new()
|
let content = Rectangle::new().width(width).height(island_pill::height()).padding(pill::inset()).align_child(Start, Center).child(Stack::new(layers).width(row_w).height(pill::slot()));
|
||||||
.width(width)
|
|
||||||
.height(island_pill::height())
|
let pill = surface::shell(width, island_pill::height(), island_pill::height() / 2.0, &theme.surfaces(), 0.0, content).on_hover(edge::over_bar);
|
||||||
.radius(Full)
|
|
||||||
.fill(backdrop(&theme.surfaces(), 255))
|
|
||||||
.border(1.0, theme.outline)
|
|
||||||
.shadow(Shadow::drop(theme.accent).opacity(0.3).blur(22.0))
|
|
||||||
.padding(pill::inset())
|
|
||||||
.align_child(Start, Center)
|
|
||||||
.on_hover(edge::over_bar)
|
|
||||||
.child(Stack::new(layers).width(row_w).height(pill::slot()));
|
|
||||||
|
|
||||||
// clicks next to the pills reach the windows underneath
|
// clicks next to the pills reach the windows underneath
|
||||||
let mut regions = vec![InputArea { x: pill::left() as i32, y: island_pill::top_gap() as i32, width: width.ceil() as i32, height: island_pill::height() as i32 }];
|
let mut regions = vec![InputArea { x: pill::left() as i32, y: island_pill::top_gap() as i32, width: width.ceil() as i32, height: island_pill::height() as i32 }];
|
||||||
|
|||||||
Reference in New Issue
Block a user