Side pills, centred: cards open big in the middle as a carousel with the other pills and the island beside them, switch directly; each pill in its own colour; row and card in separate windows
This commit is contained in:
@@ -113,7 +113,7 @@ Whether the open card takes the song cover's colours instead (`appearance.colors
|
||||
|
||||
## Side pills
|
||||
|
||||
Two small one-icon pills sit left of the island and two right of it, on every monitor; they move outward while the island opens. A click morphs a pill into its card. Only one card is open across the shell: opening the island closes a card, and opening a card closes the island. Esc, the cross or a click elsewhere closes it.
|
||||
Two small one-icon pills sit left of the island and two right of it, on every monitor, each in its own colour (`pills.colors.*`; Home's follows the sky). A click grows a pill into a big card in the middle of the top of the screen; meanwhile the row becomes a carousel: the other pills and the island's pill line up left and right of the card in their usual order, and clicking one switches straight to its card (the island's pill closes the card, then opens the island). A closing card shrinks back into its own pill's place. Only one card is open across the shell. Esc, the cross or a click elsewhere closes it.
|
||||
|
||||
- **AI** (sparkle): a chat with a local Ollama model, `gemma4:e4b` at `http://localhost:11434` by default; the model chip in its header lists the installed models and switches at once. Answers stream in through curl. When Ollama doesn't answer, the shell starts the OpenRC service with `doas rc-service ollama start` (`pills.ai.start_service`). Stop ends an answer and New chat starts over. Nothing leaves the machine.
|
||||
- **System** (pulse): CPU, the integrated GPU's clock, the external GPU's load (with its video memory and the programs on it, formerly in the control centre), memory, swap, network and disk read/write as graphs over the last minute, plus the hottest sensors with icons, in cool/warm/hot colours (`pills.monitor.warm|hot`). Everything is read from `/proc` and `/sys`, so there is no process list. Processes opens `ghostty --class=amane.monitor -e btm` on the special workspace `monitor`; a second press shows or hides it.
|
||||
|
||||
+24
-11
@@ -499,25 +499,38 @@ settings! {
|
||||
layout {
|
||||
gap: f32 = 10.0, 0..=200; "Space between the island's pill and the small pills beside it"
|
||||
spacing: f32 = 8.0, 0..=100; "Space between the small pills on one side"
|
||||
icon: f32 = 16.0, 6..=40; "Size of the small pills' icons"
|
||||
icon: f32 = 18.0, 6..=40; "Size of the small pills' icons"
|
||||
open_ms: u64 = 420, 0..=5000; "How long a small pill takes to open into its card, and to close"
|
||||
radius: f32 = 22.0, 0..=60; "Corner radius of the opened cards"
|
||||
pad: f32 = 18.0, 0..=60; "Padding inside the opened cards"
|
||||
reveal_at: f32 = 0.2, 0..=1; "How much of a closing card's growth is left when the pills beside it start to show again behind it (0 = only once it's closed)"
|
||||
radius: f32 = 28.0, 0..=60; "Corner radius of the opened cards"
|
||||
pad: f32 = 24.0, 0..=60; "Padding inside the opened cards"
|
||||
}
|
||||
colors {
|
||||
ai: Color = Color::rgb(160, 128, 255); "The AI pill's own colour, mixed into its pill and card"
|
||||
monitor: Color = Color::rgb(64, 208, 190); "The system monitor pill's own colour"
|
||||
notifications: Color = Color::rgb(255, 128, 104); "The notification pill's own colour"
|
||||
home_clear_day: Color = Color::rgb(255, 190, 92); "The home pill's colour under a clear or partly cloudy day sky"
|
||||
home_clear_night: Color = Color::rgb(104, 118, 230); "The home pill's colour on a clear night"
|
||||
home_cloudy: Color = Color::rgb(150, 170, 200); "The home pill's colour under clouds (and before the weather is known)"
|
||||
home_fog: Color = Color::rgb(176, 176, 190); "The home pill's colour in fog"
|
||||
home_rain: Color = Color::rgb(84, 156, 244); "The home pill's colour in rain"
|
||||
home_snow: Color = Color::rgb(206, 232, 255); "The home pill's colour in snow"
|
||||
home_storm: Color = Color::rgb(160, 104, 230); "The home pill's colour in a storm"
|
||||
pill_mix: f32 = 0.55, 0..=1; "How much of its own colour a small pill takes (0 = the wallpaper's only)"
|
||||
card_mix: f32 = 0.28, 0..=1; "How much of its pill's colour an open card takes"
|
||||
}
|
||||
ai {
|
||||
width: f32 = 440.0, 280..=1200; "Width of the AI card"
|
||||
height: f32 = 520.0, 200..=1200; "Height of the AI card"
|
||||
width: f32 = 760.0, 280..=1400; "Width of the AI card"
|
||||
height: f32 = 680.0, 200..=1200; "Height of the AI card"
|
||||
server: String = String::from("http://localhost:11434"); "The Ollama server the AI card talks to"
|
||||
model: String = String::from("gemma4:e4b"); "The Ollama model to chat with, with its tag (ollama list shows them)"
|
||||
start_service: bool = true; "Start the local ollama service (doas rc-service ollama start) when it isn't answering"
|
||||
system: String = String::from("You are a helpful assistant inside a Linux desktop shell. Answer briefly and plainly."); "What the model is told before every chat"
|
||||
}
|
||||
monitor {
|
||||
width: f32 = 420.0, 280..=1200; "Width of the system monitor card"
|
||||
width: f32 = 620.0, 280..=1400; "Width of the system monitor card"
|
||||
interval_ms: u64 = 1000, 200..=10000; "How often the system monitor reads the system"
|
||||
history: usize = 60, 10..=600; "How many readings the graphs show"
|
||||
graph_height: f32 = 34.0, 10..=200; "Height of each graph"
|
||||
graph_height: f32 = 46.0, 10..=200; "Height of each graph"
|
||||
temperatures: usize = 6, 0..=20; "How many temperature sensors to show, hottest first"
|
||||
warm: f32 = 65.0, 20..=120; "From this temperature (°C) a sensor shows in the warm colour"
|
||||
hot: f32 = 85.0, 20..=130; "From this temperature (°C) a sensor shows in the hot colour"
|
||||
@@ -530,7 +543,7 @@ settings! {
|
||||
workspace: String = String::from("processes"); "The special workspace the process list opens on"
|
||||
}
|
||||
home {
|
||||
width: f32 = 820.0, 300..=1600; "Width of the home card. It opens rightwards from beside the island, so on a 1920 px wide monitor about 820 fits"
|
||||
width: f32 = 880.0, 300..=1600; "Width of the home card. It opens in the middle with the AI, system and island pills on its left, which on a 1920 px wide monitor leaves room for about 900 beside the workspace pill"
|
||||
clock_size: f32 = 64.0, 20..=200; "Size of the big clock"
|
||||
seconds: bool = false; "Show seconds on the big clock"
|
||||
avatar: String = String::from("~/.face"); "The picture shown as the user's avatar; a drawn person without one"
|
||||
@@ -588,8 +601,8 @@ settings! {
|
||||
blocked: String = String::new(); "Apps whose notifications are dropped at once, comma-separated (Block app on a notification adds it)"
|
||||
}
|
||||
history {
|
||||
width: f32 = 420.0, 280..=1200; "Width of the notification card"
|
||||
height: f32 = 560.0, 200..=1400; "Height of the notification card"
|
||||
width: f32 = 620.0, 280..=1400; "Width of the notification card"
|
||||
height: f32 = 700.0, 200..=1400; "Height of the notification card"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +350,11 @@ fn sync_settings() -> (String, String, String, bool) {
|
||||
}
|
||||
|
||||
// whether the card has a form to type into: then it keeps the keyboard and stays open
|
||||
// the sky now and whether it's day, once the weather is known (the home pill takes its colour from it)
|
||||
pub fn sky() -> Option<(parse::Sky, bool)> {
|
||||
Forecast::read().weather.as_ref().map(|weather| (parse::sky(weather.code), weather.is_day))
|
||||
}
|
||||
|
||||
pub fn typing() -> bool {
|
||||
Agenda::read().form.is_some()
|
||||
}
|
||||
|
||||
+34
-3
@@ -225,8 +225,12 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
||||
.space(Zone::Ignore)
|
||||
.namespace("magic-island");
|
||||
|
||||
// out of sight: empty and click-through (never hidden, see below), and nothing else read, so a playing player doesn't redraw it
|
||||
if slide <= -edge::hide_h() {
|
||||
/*
|
||||
* out of sight: empty and click-through (never hidden, see below), and nothing else
|
||||
* read, so a playing player doesn't redraw it. also while a side pill's card is open in
|
||||
* the middle: the pods' window draws this pill in the row beside it meanwhile
|
||||
*/
|
||||
if slide <= -edge::hide_h() || crate::pods::holds_island(here) {
|
||||
return window.click_through().child(Rectangle::new().width(0.0).height(0.0));
|
||||
}
|
||||
|
||||
@@ -369,8 +373,35 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* the island's pill as the side pills' carousel draws it while one of their cards is
|
||||
* open in the middle (the island's own window leaves it to them meanwhile, see
|
||||
* pods::holds_island): the same pill with its live levels, or the quiet one
|
||||
*/
|
||||
pub fn pill_face(monitor: &str) -> Rectangle {
|
||||
let media = Media::read();
|
||||
let island = Island::read();
|
||||
let colors = theme::now().surfaces();
|
||||
|
||||
let Some(player) = shown(&media, island.selected.as_deref()) else {
|
||||
return idle(&colors);
|
||||
};
|
||||
|
||||
let live = focused_monitor().as_deref().is_none_or(|name| name == monitor);
|
||||
let spectrum = live.then(Spectrum::read);
|
||||
let levels: &[f32] = spectrum.as_deref().map_or(&[], |spectrum| spectrum.levels.as_slice());
|
||||
|
||||
if !levels.is_empty() {
|
||||
amane::request_frame();
|
||||
}
|
||||
|
||||
let face = compact(player, playing(player, &island), &Art::read().path, levels);
|
||||
|
||||
surface::shell(c::pill::width(), c::pill::height(), c::pill::height() / 2.0, &colors, 0.0, face)
|
||||
}
|
||||
|
||||
// a click on the island of `monitor`: closes an open dropdown, else opens the card there
|
||||
fn tap(monitor: &str) {
|
||||
pub fn tap(monitor: &str) {
|
||||
let mut island = Island::write();
|
||||
|
||||
if island.menu.take().is_none() {
|
||||
|
||||
+3
-3
@@ -46,9 +46,9 @@ fn main() {
|
||||
.window_per_monitor(edge::spacer)
|
||||
.window_per_monitor(edge::view)
|
||||
.window_per_monitor(island::view)
|
||||
// the small pills beside the island, and their cards
|
||||
.window_per_monitor(pods::left)
|
||||
.window_per_monitor(pods::right)
|
||||
// the small pills beside the island, and their cards in the middle
|
||||
.window_per_monitor(pods::row_view)
|
||||
.window_per_monitor(pods::card_view)
|
||||
.window_per_monitor(workspaces::view)
|
||||
.window_per_monitor(control::view)
|
||||
.window_per_monitor(control::menu_view)
|
||||
|
||||
+327
-122
@@ -8,20 +8,27 @@ use amane::{
|
||||
};
|
||||
|
||||
use crate::config::appearance::{motion, overlays, text};
|
||||
use crate::config::island::{card as island_card, pill as island_pill};
|
||||
use crate::config::island::pill as island_pill;
|
||||
use crate::config::notifications::history;
|
||||
use crate::config::pills::{self as settings, layout};
|
||||
use crate::config::pills::{self as settings, colors as hues, layout};
|
||||
use crate::edge::{self, Edge};
|
||||
use crate::glyph::{Glyph, icon};
|
||||
use crate::island::{self, CLEAR, mix, phases, veiled};
|
||||
use crate::parse::Sky;
|
||||
use crate::{ai, home, notices, stats, surface, theme};
|
||||
|
||||
/*
|
||||
* four small one-icon pills beside the island, each opening into a card the way the
|
||||
* island does: left of it the AI chat and the system monitor, right of it home and
|
||||
* notifications. one window per side and monitor (magic-pods-left|right) placed beside
|
||||
* the island's pill; while the island opens they move out of its way. one card at a
|
||||
* time across the shell: opening one collapses the island and the other way round
|
||||
* four small one-icon pills beside the island, each opening into a big card in the
|
||||
* middle of the top of the screen. while a card is open the row becomes a carousel:
|
||||
* every other pill and the island's pill line up left and right of the card in their
|
||||
* usual order ([AI][System] island [Home][Bell]), and clicking one switches to its card.
|
||||
* a closing card shrinks back into its own pill's place.
|
||||
*
|
||||
* one window per monitor (magic-pods), as wide as the monitor and anchored to its left
|
||||
* edge, so it never moves: only its height changes, at the top edge. while a card is
|
||||
* open it also draws the island's pill (island::pill_face), which the island's own
|
||||
* window leaves out meanwhile (holds_island); the two overlap for a moment at each
|
||||
* hand-over, at the same place, so the pill never blinks
|
||||
*/
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum Pod {
|
||||
@@ -31,15 +38,18 @@ pub enum Pod {
|
||||
Notifications,
|
||||
}
|
||||
|
||||
// left of the island, outermost first; right of it, innermost first
|
||||
const LEFT: [Pod; 2] = [Pod::Ai, Pod::Monitor];
|
||||
const RIGHT: [Pod; 2] = [Pod::Home, Pod::Notifications];
|
||||
// the row, left to right
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Item {
|
||||
Pod(Pod),
|
||||
Island,
|
||||
}
|
||||
|
||||
const ROW: [Item; 5] = [Item::Pod(Pod::Ai), Item::Pod(Pod::Monitor), Item::Island, Item::Pod(Pod::Home), Item::Pod(Pod::Notifications)];
|
||||
|
||||
const PODS: [Pod; 4] = [Pod::Ai, Pod::Monitor, Pod::Home, Pod::Notifications];
|
||||
|
||||
impl Pod {
|
||||
fn left(self) -> bool {
|
||||
matches!(self, Self::Ai | Self::Monitor)
|
||||
}
|
||||
|
||||
fn glyph(self) -> Glyph {
|
||||
match self {
|
||||
Self::Ai => Glyph::Sparkle,
|
||||
@@ -59,6 +69,24 @@ impl Pod {
|
||||
}
|
||||
}
|
||||
|
||||
// its own colour; home's follows the sky
|
||||
fn hue(self) -> Color {
|
||||
match self {
|
||||
Self::Ai => hues::ai(),
|
||||
Self::Monitor => hues::monitor(),
|
||||
Self::Notifications => hues::notifications(),
|
||||
Self::Home => match home::sky() {
|
||||
Some((Sky::Clear | Sky::PartlyCloudy, true)) => hues::home_clear_day(),
|
||||
Some((Sky::Clear | Sky::PartlyCloudy, false)) => hues::home_clear_night(),
|
||||
Some((Sky::Cloudy, _)) | None => hues::home_cloudy(),
|
||||
Some((Sky::Fog, _)) => hues::home_fog(),
|
||||
Some((Sky::Rain, _)) => hues::home_rain(),
|
||||
Some((Sky::Snow, _)) => hues::home_snow(),
|
||||
Some((Sky::Storm, _)) => hues::home_storm(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// whether its card takes typing: then it has the keyboard and stays open when the pointer leaves
|
||||
fn types(self) -> bool {
|
||||
self == Self::Ai || (self == Self::Home && home::typing())
|
||||
@@ -74,16 +102,25 @@ impl Pod {
|
||||
}
|
||||
|
||||
fn from_word(word: &str) -> Option<Self> {
|
||||
[Self::Ai, Self::Monitor, Self::Home, Self::Notifications].into_iter().find(|pod| pod.word() == word)
|
||||
PODS.into_iter().find(|pod| pod.word() == word)
|
||||
}
|
||||
}
|
||||
|
||||
// the wallpaper's surfaces with some of a colour mixed in, keeping their see-through
|
||||
fn tinted(colors: &[Color; 3], hue: Color, amount: f32) -> [Color; 3] {
|
||||
colors.map(|color| Color::blend(color, Color::rgba(hue.red(), hue.green(), hue.blue(), color.alpha()), amount))
|
||||
}
|
||||
|
||||
pub struct Pods {
|
||||
// the card open or closing, and the monitor it's on; `shown` whether it's opening or open
|
||||
card: Option<(Pod, String)>,
|
||||
shown: bool,
|
||||
timeline: Animation,
|
||||
|
||||
// a switch from one open card straight to another: the one before, and the switch's progress
|
||||
previous: Option<Pod>,
|
||||
switch: Animation,
|
||||
|
||||
hover: HashMap<Pod, Animation>,
|
||||
|
||||
// the pointer over the card, and a count every arrival and departure bumps, so a late close does nothing
|
||||
@@ -93,7 +130,7 @@ pub struct Pods {
|
||||
|
||||
impl Service for Pods {
|
||||
fn new() -> Self {
|
||||
Self { card: None, shown: false, timeline: timeline(), hover: HashMap::new(), inside: false, moves: 0 }
|
||||
Self { card: None, shown: false, timeline: timeline(), previous: None, switch: switching(1.0), hover: HashMap::new(), inside: false, moves: 0 }
|
||||
}
|
||||
|
||||
// changes only through input
|
||||
@@ -104,15 +141,34 @@ fn timeline() -> Animation {
|
||||
Animation::new(0.0).duration(Duration::from_millis(layout::open_ms())).easing(Easing::Linear)
|
||||
}
|
||||
|
||||
fn switching(from: f32) -> Animation {
|
||||
Animation::new(from).duration(Duration::from_millis(layout::open_ms())).easing(Easing::InOut)
|
||||
}
|
||||
|
||||
pub fn open(pod: Pod, monitor: &str) {
|
||||
island::collapse();
|
||||
|
||||
let mut pods = Pods::write();
|
||||
|
||||
// another card, or this one on another monitor: it grows from its own pill
|
||||
if pods.card.as_ref().is_none_or(|(open, on)| *open != pod || on != monitor) {
|
||||
pods.card = Some((pod, monitor.to_string()));
|
||||
pods.timeline = timeline();
|
||||
let open_here = pods.shown && pods.timeline.value() > 0.5;
|
||||
let current = pods.card.as_ref().filter(|(_, on)| on == monitor).map(|(open, _)| *open);
|
||||
|
||||
match current {
|
||||
Some(open) if open == pod => {}
|
||||
// another card while one is open here: the open one turns into it
|
||||
Some(open) if open_here => {
|
||||
pods.previous = Some(open);
|
||||
pods.switch = switching(0.0);
|
||||
pods.switch.to(1.0);
|
||||
pods.card = Some((pod, monitor.to_string()));
|
||||
}
|
||||
// another card, or one on another monitor: it grows from its own pill
|
||||
_ => {
|
||||
pods.card = Some((pod, monitor.to_string()));
|
||||
pods.timeline = timeline();
|
||||
pods.previous = None;
|
||||
pods.switch = switching(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
pods.shown = true;
|
||||
@@ -152,6 +208,20 @@ pub fn open_card() -> Option<Pod> {
|
||||
pods.card.as_ref().filter(|_| pods.shown).map(|(pod, _)| *pod)
|
||||
}
|
||||
|
||||
/*
|
||||
* whether this window draws the island's pill on `monitor` now: a little past the start
|
||||
* of opening and until a little before the end of closing. this window draws it from
|
||||
* the first frame to the last, so the two overlap at each hand-over, at the same place
|
||||
*/
|
||||
pub fn holds_island(monitor: &str) -> bool {
|
||||
let pods = Pods::read();
|
||||
|
||||
pods.card.as_ref().is_some_and(|(_, on)| on == monitor) && pods.timeline.value() > HAND_OVER
|
||||
}
|
||||
|
||||
// a share of the open timeline (420 ms by default: 0.08 is about two frames)
|
||||
const HAND_OVER: f32 = 0.08;
|
||||
|
||||
fn hovering(pod: Pod) -> impl Fn(bool) + 'static {
|
||||
move |inside| {
|
||||
edge::over_bar(inside);
|
||||
@@ -195,138 +265,272 @@ fn over_card(inside: bool) {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn left(monitor: &Monitor) -> LayerWindow {
|
||||
side(monitor, true)
|
||||
// the island's pill in the carousel: the card closes back into place, then the island opens
|
||||
fn island_tapped(monitor: String) {
|
||||
close();
|
||||
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(layout::open_ms()));
|
||||
|
||||
island::tap(&monitor);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn right(monitor: &Monitor) -> LayerWindow {
|
||||
side(monitor, false)
|
||||
fn item_width(item: Item) -> f32 {
|
||||
match item {
|
||||
Item::Pod(_) => island_pill::height(),
|
||||
Item::Island => island_pill::width(),
|
||||
}
|
||||
}
|
||||
|
||||
fn side(monitor: &Monitor, left: bool) -> LayerWindow {
|
||||
// an item's left edge in the row with nothing open, around the island as wide as it is now
|
||||
fn closed_x(item: Item, middle: f32, island_w: f32) -> f32 {
|
||||
let (h, spacing, gap) = (island_pill::height(), layout::spacing(), layout::gap());
|
||||
|
||||
match item {
|
||||
Item::Island => middle - island_pill::width() / 2.0,
|
||||
Item::Pod(Pod::Monitor) => middle - island_w / 2.0 - gap - h,
|
||||
Item::Pod(Pod::Ai) => middle - island_w / 2.0 - gap - 2.0 * h - spacing,
|
||||
Item::Pod(Pod::Home) => middle + island_w / 2.0 + gap,
|
||||
Item::Pod(Pod::Notifications) => middle + island_w / 2.0 + gap + h + spacing,
|
||||
}
|
||||
}
|
||||
|
||||
// an item's left edge in the carousel around `open`'s card, `card_w` wide, in the middle
|
||||
fn carousel_x(item: Item, open: Pod, card_w: f32, middle: f32) -> f32 {
|
||||
let at = |wanted: Item| ROW.iter().position(|other| *other == wanted).unwrap_or(0);
|
||||
let (index, centre) = (at(item), at(Item::Pod(open)));
|
||||
let (spacing, gap) = (layout::spacing(), layout::gap());
|
||||
|
||||
if index == centre {
|
||||
return middle - card_w / 2.0;
|
||||
}
|
||||
|
||||
if index < centre {
|
||||
// leftwards from the card, past the items between
|
||||
let between: f32 = ROW[index + 1..centre].iter().map(|other| item_width(*other) + spacing).sum();
|
||||
|
||||
middle - card_w / 2.0 - gap - between - item_width(item)
|
||||
} else {
|
||||
let between: f32 = ROW[centre + 1..index].iter().map(|other| item_width(*other) + spacing).sum();
|
||||
|
||||
middle + card_w / 2.0 + gap + between
|
||||
}
|
||||
}
|
||||
|
||||
// what both windows need to know about this monitor's card this frame
|
||||
struct Scene {
|
||||
// the pod and its timeline, while it's at all open here
|
||||
card: Option<(Pod, f32)>,
|
||||
grow: f32,
|
||||
pill_shown: f32,
|
||||
reveal: f32,
|
||||
switch: f32,
|
||||
previous: Option<Pod>,
|
||||
middle: f32,
|
||||
island_w: f32,
|
||||
// the island's pill starts moving only after the hand-over (see holds_island), so it never shows twice
|
||||
island_grow: f32,
|
||||
}
|
||||
|
||||
fn scene(state: &Pods, monitor: &Monitor) -> Scene {
|
||||
let here = monitor.name.as_str();
|
||||
let card = state.card.as_ref().filter(|(_, on)| on == here).map(|(pod, _)| (*pod, state.timeline.value())).filter(|(_, t)| *t > 0.0);
|
||||
let (grow, pill_shown, reveal) = card.map_or((0.0, 1.0, 0.0), |(_, t)| phases(t));
|
||||
|
||||
// read only here: a running animation redraws every window that reads it
|
||||
let switch = if card.is_some() { state.switch.value() } else { 1.0 };
|
||||
let previous = state.previous.filter(|_| switch < 1.0 && card.is_some());
|
||||
|
||||
let island_grow = card.map_or(0.0, |(_, t)| phases(((t - 2.0 * HAND_OVER) / (1.0 - 2.0 * HAND_OVER)).max(0.0)).0);
|
||||
|
||||
Scene { card, grow, pill_shown, reveal, switch, previous, middle: monitor.width as f32 / 2.0, island_w: island::width_on(here), island_grow }
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
// an item's left edge on the monitor: between the closed row and the carousel by `grow`, between two carousels by `switch`
|
||||
fn place(&self, item: Item) -> f32 {
|
||||
let closed = closed_x(item, self.middle, self.island_w);
|
||||
|
||||
let Some((open, _)) = self.card else { return closed };
|
||||
|
||||
let now = carousel_x(item, open, open.size().0, self.middle);
|
||||
let open_x = match self.previous {
|
||||
Some(before) => mix(carousel_x(item, before, before.size().0, self.middle), now, self.switch),
|
||||
None => now,
|
||||
};
|
||||
|
||||
mix(closed, open_x, if item == Item::Island { self.island_grow } else { self.grow })
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* the row of pills: as wide as the monitor and as tall as a pill, never changing. amane
|
||||
* repaints a whole window every frame and hyprland then recomposites (and re-blurs)
|
||||
* everything under it, so a window as tall as the cards ran the animations at 15 fps;
|
||||
* the cards have their own window (card_view)
|
||||
*/
|
||||
pub fn row_view(monitor: &Monitor) -> LayerWindow {
|
||||
theme::begin();
|
||||
|
||||
let here = monitor.name.as_str();
|
||||
let pods = if left { LEFT } else { RIGHT };
|
||||
let colors = theme::now().surfaces();
|
||||
|
||||
let wallpaper = theme::now().surfaces();
|
||||
let state = Pods::read();
|
||||
let scene = scene(&state, monitor);
|
||||
let (h, top, shadow) = (island_pill::height(), island_pill::top_gap(), island_pill::shadow());
|
||||
|
||||
// this side's card on this monitor, while it's at all open
|
||||
let card = state.card.as_ref().filter(|(pod, on)| pod.left() == left && on == here).map(|(pod, _)| (*pod, state.timeline.value())).filter(|(_, t)| *t > 0.0);
|
||||
|
||||
// with the bar on a fullscreen monitor, unless its card is open; out of the island's way while it opens
|
||||
let slide = if card.is_some() { 0.0 } else { edge::hidden_by(Edge::read().shown(here), 0.0, edge::hide_h()) };
|
||||
let shift = (island::width_on(here) - island_pill::width()) / 2.0;
|
||||
|
||||
let (h, top, shadow, spacing) = (island_pill::height(), island_pill::top_gap(), island_pill::shadow(), layout::spacing());
|
||||
let count = pods.len() as f32;
|
||||
let group_w = count * h + (count - 1.0) * spacing;
|
||||
|
||||
let (card_w, card_h) = card.map_or((0.0, 0.0), |(pod, _)| pod.size());
|
||||
|
||||
/*
|
||||
* room for the pills to move away from the open island, or for this side's widest
|
||||
* card. the width never follows the open card: a narrower window would move with
|
||||
* its margin, and Hyprland applies size and position on different frames, so the
|
||||
* pills jumped when a card closed
|
||||
*/
|
||||
let room = (island_card::width() - island_pill::width()) / 2.0;
|
||||
let widest = pods.iter().map(|pod| pod.size().0).fold(0.0, f32::max);
|
||||
let inner_w = (group_w + room).max(widest);
|
||||
let window_h = top + h.max(card_h) + shadow;
|
||||
|
||||
// the group's edge nearest the island, in the window, and where the window starts on the monitor
|
||||
let near = if left { shadow + inner_w } else { shadow };
|
||||
let middle = monitor.width as f32 / 2.0;
|
||||
let beside = island_pill::width() / 2.0 + layout::gap();
|
||||
let start = if left { middle - beside - near } else { middle + beside - near };
|
||||
// with the bar on a fullscreen monitor, unless a card is open
|
||||
let slide = if scene.card.is_some() { 0.0 } else { edge::hidden_by(Edge::read().shown(here), 0.0, edge::hide_h()) };
|
||||
|
||||
let window = LayerWindow::new()
|
||||
.width(inner_w + 2.0 * shadow)
|
||||
.height(window_h)
|
||||
.width(monitor.width as f32)
|
||||
.height(top + h + shadow)
|
||||
.anchor_vertical(Vertical::Top)
|
||||
.anchor_horizontal(Horizontal::Left)
|
||||
.margin(Margin { top: 0, right: 0, bottom: 0, left: start.round() as i32 })
|
||||
.margin(Margin { top: 0, right: 0, bottom: 0, left: 0 })
|
||||
.layer(Layer::Overlay)
|
||||
.space(Zone::Ignore)
|
||||
.namespace(if left { "magic-pods-left" } else { "magic-pods-right" });
|
||||
.namespace("magic-pods");
|
||||
|
||||
// empty and click-through rather than hidden: see the island's view for why
|
||||
if slide <= -edge::hide_h() {
|
||||
return window.click_through().child(Rectangle::new().width(0.0).height(0.0));
|
||||
}
|
||||
|
||||
// a pill's left edge in the window; `k` counts from the island outwards
|
||||
let pill_x = |pod: Pod| {
|
||||
let at = pods.iter().position(|other| *other == pod).unwrap_or(0) as f32;
|
||||
let k = if left { count - 1.0 - at } else { at };
|
||||
|
||||
if left { near - shift - (k + 1.0) * h - k * spacing } else { near + shift + k * (h + spacing) }
|
||||
};
|
||||
|
||||
let (grow, pill_shown, reveal) = card.map_or((0.0, 1.0, 0.0), |(_, t)| phases(t));
|
||||
|
||||
/*
|
||||
* the pills beside an open card are gone; a closing card shrinks back into its own
|
||||
* pill's place, and they come back behind it only over the last `reveal_at` of its
|
||||
* shrinking, so they don't pop up around a big card
|
||||
*/
|
||||
let reveal_at = layout::reveal_at();
|
||||
let others = if grow <= 0.0 {
|
||||
1.0
|
||||
} else if reveal_at <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
let k = (1.0 - grow / reveal_at).clamp(0.0, 1.0);
|
||||
|
||||
k * k * (3.0 - 2.0 * k)
|
||||
};
|
||||
let (card, previous, switch) = (scene.card, scene.previous, scene.switch);
|
||||
|
||||
let mut layers: Vec<Box<dyn Widget>> = Vec::new();
|
||||
let mut regions: Vec<InputArea> = Vec::new();
|
||||
|
||||
for pod in pods {
|
||||
// the open one is the card, drawn last, over the others
|
||||
if card.is_some_and(|(open, _)| open == pod) || others <= 0.0 {
|
||||
continue;
|
||||
for item in ROW {
|
||||
let x = scene.place(item);
|
||||
|
||||
match item {
|
||||
Item::Island => {
|
||||
// the island's own window draws its pill while nothing is open here
|
||||
if card.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let on = here.to_string();
|
||||
let pill = island::pill_face(here).cursor(Pointer).on_hover(edge::over_bar).on_click(move |_| island_tapped(on.clone()));
|
||||
|
||||
regions.push(InputArea { x: x as i32, y: top as i32, width: island_pill::width().ceil() as i32, height: h.ceil() as i32 });
|
||||
layers.push(Box::new(pill.translate(x, top)));
|
||||
}
|
||||
Item::Pod(pod) => {
|
||||
// the open one is the card, drawn last. in a switch, the one switched to slides into
|
||||
// the card and fades over the first half; the one switched away from fades back in as a pill
|
||||
let shown = match previous {
|
||||
Some(_) if card.is_some_and(|(open, _)| open == pod) => 1.0 - 2.0 * switch,
|
||||
// its card is in the card window, which starts drawing at HAND_OVER; the pill stays a little longer under it
|
||||
_ if card.is_some_and(|(open, t)| open == pod && t > 2.0 * HAND_OVER) => continue,
|
||||
Some(before) if before == pod => switch,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
if shown <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lit = state.hover.get(&pod).map_or(0.0, Animation::value);
|
||||
let pill = pill(pod, lit, &wallpaper, here);
|
||||
let pill = if shown < 1.0 { pill.opacity(shown) } else { pill };
|
||||
|
||||
regions.push(InputArea { x: x as i32, y: (top + slide) as i32, width: h.ceil() as i32, height: h.ceil() as i32 });
|
||||
layers.push(Box::new(pill.translate(x, top + slide)));
|
||||
}
|
||||
}
|
||||
|
||||
let x = pill_x(pod);
|
||||
let lit = state.hover.get(&pod).map_or(0.0, Animation::value);
|
||||
let pill = pill(pod, lit, &colors, here);
|
||||
let pill = if others < 1.0 { pill.opacity(others) } else { pill };
|
||||
|
||||
regions.push(InputArea { x: x as i32, y: (top + slide) as i32, width: h.ceil() as i32, height: h.ceil() as i32 });
|
||||
layers.push(Box::new(pill.translate(x, top + slide)));
|
||||
}
|
||||
|
||||
let typing = card.is_some_and(|(pod, _)| pod.types() && state.shown);
|
||||
window.input_region(regions).child(Rectangle::new().width(Parent).height(Parent).child(Stack::new(layers).width(Parent).height(Parent)))
|
||||
}
|
||||
|
||||
if let Some((pod, _)) = card {
|
||||
// from the pill to the card, its edge nearest the island staying put
|
||||
let from_x = pill_x(pod);
|
||||
let to_x = if left { near - shift - card_w } else { near + shift };
|
||||
/*
|
||||
* the open card, in the middle: a window as big as the biggest card while one is open
|
||||
* here, and an empty pixel otherwise. it changes size only while it shows nothing (the
|
||||
* card starts and ends as its pill, which the row window draws under it meanwhile), so
|
||||
* hyprland's squashed first frame at a new size is never seen
|
||||
*/
|
||||
pub fn card_view(monitor: &Monitor) -> LayerWindow {
|
||||
theme::begin();
|
||||
|
||||
let width = mix(h, card_w, grow);
|
||||
let height = mix(h, card_h, grow);
|
||||
let wallpaper = theme::now().surfaces();
|
||||
let state = Pods::read();
|
||||
let scene = scene(&state, monitor);
|
||||
let (h, top, shadow) = (island_pill::height(), island_pill::top_gap(), island_pill::shadow());
|
||||
|
||||
let window = |width: f32, height: f32| LayerWindow::new().width(width).height(height).anchor_vertical(Vertical::Top).layer(Layer::Overlay).space(Zone::Ignore).namespace("magic-pods-card");
|
||||
|
||||
let Some((pod, t)) = scene.card else {
|
||||
return window(1.0, 1.0).click_through().child(Rectangle::new().width(0.0).height(0.0));
|
||||
};
|
||||
|
||||
let widest = PODS.iter().map(|pod| pod.size().0).fold(h, f32::max);
|
||||
let tallest = [settings::ai::height(), stats::tallest(), home::height(), history::height()].into_iter().fold(h, f32::max);
|
||||
let window_w = (widest + 2.0 * shadow).round();
|
||||
|
||||
// centred by hyprland: its left edge on the monitor
|
||||
let left = ((monitor.width as f32 - window_w) / 2.0).floor();
|
||||
let window = window(window_w, top + tallest + shadow);
|
||||
|
||||
// the first frames at the new size show at the old place, so they stay empty (the row draws the pill meanwhile)
|
||||
if t <= HAND_OVER {
|
||||
return window.click_through().child(Rectangle::new().width(0.0).height(0.0));
|
||||
}
|
||||
|
||||
let (grow, pill_shown, reveal, switch, previous, middle) = (scene.grow, scene.pill_shown, scene.reveal, scene.switch, scene.previous, scene.middle);
|
||||
|
||||
let mut layers: Vec<Box<dyn Widget>> = Vec::new();
|
||||
let mut regions: Vec<InputArea> = Vec::new();
|
||||
|
||||
let typing = pod.types() && state.shown;
|
||||
|
||||
{
|
||||
let (card_w, card_h) = pod.size();
|
||||
|
||||
// a switch morphs the card from the one before: its size, its colour, and its face crossing over
|
||||
let (full_w, full_h, hue) = match previous {
|
||||
Some(before) => {
|
||||
let (before_w, before_h) = before.size();
|
||||
|
||||
(mix(before_w, card_w, switch), mix(before_h, card_h, switch), Color::blend(before.hue(), pod.hue(), switch))
|
||||
}
|
||||
None => (card_w, card_h, pod.hue()),
|
||||
};
|
||||
|
||||
// from its pill's place in the closed row to the middle, and back into that place; in the window
|
||||
let from_x = closed_x(Item::Pod(pod), middle, scene.island_w) - left;
|
||||
let to_x = middle - full_w / 2.0 - left;
|
||||
|
||||
let width = mix(h, full_w, grow);
|
||||
let height = mix(h, full_h, grow);
|
||||
let x = mix(from_x, to_x, grow);
|
||||
let radius = mix(h / 2.0, layout::radius(), grow);
|
||||
let lit = state.hover.get(&pod).map_or(0.0, Animation::value);
|
||||
let colors = tinted(&wallpaper, hue, mix(hues::pill_mix(), hues::card_mix(), grow));
|
||||
|
||||
let face: Rectangle = if reveal > 0.0 {
|
||||
veiled(content(pod, card_w, card_h), width, height, radius, reveal, &colors)
|
||||
} else {
|
||||
// the pill's icon fading as the shape grows
|
||||
let tint = Color::blend(CLEAR, theme::now().text, pill_shown);
|
||||
let face: Rectangle = match previous {
|
||||
// the card before fades out over the first half of the switch, this one in over the second
|
||||
Some(before) if switch < 0.5 => {
|
||||
let (before_w, before_h) = before.size();
|
||||
|
||||
Rectangle::new().width(h).height(h).align_child(Center, Center).child(icon(pod.glyph(), layout::icon(), tint))
|
||||
veiled(content(before, before_w, before_h), width, height, radius, 1.0 - 2.0 * switch, &colors)
|
||||
}
|
||||
Some(_) => veiled(content(pod, card_w, card_h), width, height, radius, 2.0 * switch - 1.0, &colors),
|
||||
None if reveal > 0.0 => veiled(content(pod, card_w, card_h), width, height, radius, reveal, &colors),
|
||||
None => {
|
||||
// the pill's icon fading as the shape grows
|
||||
let tint = Color::blend(CLEAR, pod.hue(), pill_shown);
|
||||
|
||||
Rectangle::new().width(h).height(h).align_child(Center, Center).child(icon(pod.glyph(), layout::icon(), tint))
|
||||
}
|
||||
};
|
||||
|
||||
let face = Rectangle::new().width(width).height(height).align_child(Start, Start).child(face);
|
||||
let shape = surface::shell(width, height, radius, &colors, lit * pill_shown, face).on_hover(over_card).translate(x, top + slide);
|
||||
let face = Rectangle::new().width(width).height(height).align_child(Center, Start).child(face);
|
||||
let shape = surface::shell(width, height, radius, &colors, lit * pill_shown, face).on_hover(over_card).translate(x, top);
|
||||
|
||||
regions.push(InputArea { x: x as i32, y: (top + slide) as i32, width: width.ceil() as i32, height: height.ceil() as i32 });
|
||||
regions.push(InputArea { x: x as i32, y: top as i32, width: width.ceil() as i32, height: height.ceil() as i32 });
|
||||
layers.push(Box::new(shape));
|
||||
}
|
||||
|
||||
@@ -345,12 +549,14 @@ fn side(monitor: &Monitor, left: bool) -> LayerWindow {
|
||||
window.child(Rectangle::new().width(Parent).height(Parent).child(Stack::new(layers).width(Parent).height(Parent)))
|
||||
}
|
||||
|
||||
// a small pill: the surface with one icon; the bell carries the unread count
|
||||
fn pill(pod: Pod, lit: f32, colors: &[Color; 3], monitor: &str) -> Rectangle {
|
||||
// a small pill in its own colour: the surface with one icon; the bell carries the unread count
|
||||
fn pill(pod: Pod, lit: f32, wallpaper: &[Color; 3], monitor: &str) -> Rectangle {
|
||||
let h = island_pill::height();
|
||||
let theme = theme::now();
|
||||
let hue = pod.hue();
|
||||
let colors = tinted(wallpaper, hue, hues::pill_mix());
|
||||
|
||||
let glyph = icon(pod.glyph(), layout::icon() * (1.0 + 0.08 * lit), Color::blend(theme.dim, theme.text, 0.6 + 0.4 * lit));
|
||||
let glyph = icon(pod.glyph(), layout::icon() * (1.0 + 0.08 * lit), Color::blend(hue, theme.text, 0.25 + 0.5 * lit));
|
||||
let mut inside: Vec<Box<dyn Widget>> = vec![Box::new(Rectangle::new().width(h).height(h).align_child(Center, Center).child(glyph))];
|
||||
|
||||
let unread = if pod == Pod::Notifications { notices::unread() } else { 0 };
|
||||
@@ -364,7 +570,7 @@ fn pill(pod: Pod, lit: f32, colors: &[Color; 3], monitor: &str) -> Rectangle {
|
||||
.width(badge)
|
||||
.height(badge)
|
||||
.radius(Full)
|
||||
.fill(theme.accent)
|
||||
.fill(hue)
|
||||
.align_child(Center, Center)
|
||||
.translate(h - badge - 3.0, 3.0)
|
||||
.child(Text::new(words).size(text::tiny() - 1.0).weight(Weight::Bold).color(theme.on_accent)),
|
||||
@@ -374,7 +580,7 @@ fn pill(pod: Pod, lit: f32, colors: &[Color; 3], monitor: &str) -> Rectangle {
|
||||
let content = Rectangle::new().width(h).height(h).child(Stack::new(inside).width(h).height(h));
|
||||
let on = monitor.to_string();
|
||||
|
||||
surface::shell(h, h, h / 2.0, colors, lit, content).cursor(Pointer).on_hover(hovering(pod)).on_click(move |_| toggle(pod, &on))
|
||||
surface::shell(h, h, h / 2.0, &colors, lit, content).cursor(Pointer).on_hover(hovering(pod)).on_click(move |_| toggle(pod, &on))
|
||||
}
|
||||
|
||||
fn content(pod: Pod, width: f32, height: f32) -> Rectangle {
|
||||
@@ -493,9 +699,9 @@ pub fn command(arguments: &[String]) -> String {
|
||||
let card = pods.card.as_ref().filter(|_| pods.shown).map_or("none", |(pod, _)| pod.word());
|
||||
let on = pods.card.as_ref().map_or("", |(_, on)| on.as_str());
|
||||
|
||||
format!("card={card} monitor={on} timeline={:.2} unread={}", pods.timeline.value(), notices::unread())
|
||||
format!("card={card} monitor={on} timeline={:.2} switch={:.2} unread={}", pods.timeline.value(), pods.switch.value(), notices::unread())
|
||||
}
|
||||
_ => String::from("usage: pods open|toggle <ai|monitor|home|notifications> [monitor] | close | ask <question> | notices list|dismiss <id>|clear | status"),
|
||||
_ => String::from("usage: pods open|toggle <ai|monitor|home|notifications> [monitor] | close | ask <question> | processes | notices list|dismiss <id>|clear | status"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,4 +709,3 @@ pub fn command(arguments: &[String]) -> String {
|
||||
pub fn rule(width: f32) -> Rectangle {
|
||||
Rectangle::new().width(width).height(1.0).fill(overlays::track())
|
||||
}
|
||||
|
||||
|
||||
@@ -285,6 +285,14 @@ pub fn height() -> f32 {
|
||||
2.0 * layout::pad() + pods::header_height() + graphs + egpu + 1.0 + label_height() + temp_rows() as f32 * temp_row_height() + (parts - 1.0) * 10.0
|
||||
}
|
||||
|
||||
// the card's height at its tallest: an external gpu with its whole list of programs
|
||||
pub fn tallest() -> f32 {
|
||||
let graphs = 7.0 * (label_height() + settings::graph_height()) + 6.0 * 10.0;
|
||||
let egpu = (1 + settings::programs()) as f32 * program_row_height();
|
||||
|
||||
2.0 * layout::pad() + pods::header_height() + graphs + egpu + 1.0 + label_height() + temp_rows() as f32 * temp_row_height() + 5.0 * 10.0
|
||||
}
|
||||
|
||||
// bytes a second in words
|
||||
fn rate(bytes: f32) -> String {
|
||||
match bytes {
|
||||
|
||||
Reference in New Issue
Block a user