Carousel: always two items on each side of an open card, in the row's order going round (items changing sides fade instead of crossing); the island shrinks to its cover, or its bars

This commit is contained in:
server2
2026-10-16 02:56:15 +02:00
parent 8cbd211da0
commit 7cb2368b23
2 changed files with 142 additions and 50 deletions
+53 -19
View File
@@ -315,7 +315,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
veiled(card(&scene), width, height, radius, reveal, &colors)
} else if pill > 0.0 {
veiled(compact(player, playing(player, &island), &art.path, levels), width, height, radius, pill, &colors)
veiled(compact(player, playing(player, &island), &art.path, levels, c::pill::width()), width, height, radius, pill, &colors)
} else {
// mid-grow: the empty shape alone, the cheapest frames of the animation
Rectangle::new().width(0.0).height(0.0)
@@ -376,18 +376,21 @@ 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
* pods::holds_island). `small` shrinks it from the pill (0) to its least (1): the song's
* cover, or its live bars when there's no cover, or a note with nothing playing. the
* pill's face fades out over the first half, the small one in over the second
*/
pub fn pill_face(monitor: &str) -> Rectangle {
pub fn pill_face(monitor: &str, small: f32) -> Rectangle {
let media = Media::read();
let island = Island::read();
let colors = theme::now().surfaces();
let art = Art::read();
let h = c::pill::height();
let Some(player) = shown(&media, island.selected.as_deref()) else {
return idle(&colors);
};
let player = shown(&media, island.selected.as_deref());
let width = mix(c::pill::width(), small_width_for(player.is_some(), &art.path), small);
let live = focused_monitor().as_deref().is_none_or(|name| name == monitor);
let live = player.is_some() && 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());
@@ -395,9 +398,38 @@ pub fn pill_face(monitor: &str) -> Rectangle {
amane::request_frame();
}
let face = compact(player, playing(player, &island), &Art::read().path, levels);
let face = if small < 0.5 {
let big = match player {
Some(player) => compact(player, playing(player, &island), &art.path, levels, width),
None => idle_face(width),
};
surface::shell(c::pill::width(), c::pill::height(), c::pill::height() / 2.0, &colors, 0.0, face)
veiled(big, width, h, h / 2.0, 1.0 - 2.0 * small, &colors)
} else {
let little: Box<dyn Widget> = match player {
Some(_) if !art.path.is_empty() => Box::new(cover(&art.path, h - 10.0, 1.5, false)),
Some(player) => Box::new(pill_levels(playing(player, &island), levels)),
None => Box::new(icon(Glyph::Note, 15.0, theme::now().accent)),
};
veiled(Rectangle::new().width(width).height(h).align_child(Center, Center).child(Stack::new(vec![little])), width, h, h / 2.0, 2.0 * small - 1.0, &colors)
};
surface::shell(width, h, h / 2.0, &colors, 0.0, face)
}
// the island's pill at its least in the carousel: as wide as tall with a cover or nothing playing, else room for its bars
pub fn small_width() -> f32 {
let media = Media::read();
let island = Island::read();
small_width_for(shown(&media, island.selected.as_deref()).is_some(), &Art::read().path)
}
fn small_width_for(playing_something: bool, art: &str) -> f32 {
let h = c::pill::height();
if !playing_something || !art.is_empty() { h } else { (c::pill::bars_width() + 24.0).max(h) }
}
// a click on the island of `monitor`: closes an open dropdown, else opens the card there
@@ -633,19 +665,21 @@ pub fn faded(color: Color, alpha: u8) -> Color {
// the island while no player is open: a music note and "Nothing playing", on the pill's surface
fn idle(colors: &[Color; 3]) -> Rectangle {
let content = Rectangle::new()
.width(c::pill::width())
.height(c::pill::height())
.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));
surface::shell(c::pill::width(), c::pill::height(), c::pill::height() / 2.0, colors, 0.0, content)
surface::shell(c::pill::width(), c::pill::height(), c::pill::height() / 2.0, colors, 0.0, idle_face(c::pill::width()))
}
// the pill: cover, title, the player's live levels
fn compact(player: &MediaPlayer, playing: bool, art: &str, levels: &[f32]) -> Rectangle {
fn idle_face(width: f32) -> Rectangle {
Rectangle::new()
.width(c::pill::width())
.width(width)
.height(c::pill::height())
.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).elide()]).gap(8.0).align(Center))
}
// the pill, `width` wide: cover, title, the player's live levels
fn compact(player: &MediaPlayer, playing: bool, art: &str, levels: &[f32], width: f32) -> Rectangle {
Rectangle::new()
.width(width)
.height(c::pill::height())
.padding(Padding { top: 0.0, right: 12.0, bottom: 0.0, left: 6.0 })
.child(
+90 -32
View File
@@ -276,10 +276,12 @@ fn island_tapped(monitor: String) {
});
}
// an item's width in the carousel
fn item_width(item: Item) -> f32 {
match item {
Item::Pod(_) => island_pill::height(),
Item::Island => island_pill::width(),
// beside an open card the island's pill shrinks to its cover or its bars
Item::Island => island::small_width(),
}
}
@@ -296,25 +298,65 @@ fn closed_x(item: Item, middle: f32, island_w: f32) -> f32 {
}
}
// 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;
fn index(item: Item) -> usize {
ROW.iter().position(|other| *other == item).unwrap_or(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();
/*
* the two items on each side of `open`'s card, always: the row's order going round, so
* the ones past an end of the row come in on the other side (around Home: System and the
* island on its left, the bell and AI on its right)
*/
fn beside(open: Pod) -> ([Item; 2], [Item; 2]) {
let (count, at) = (ROW.len(), index(Item::Pod(open)));
let step = |by: usize| ROW[(at + by) % count];
middle - card_w / 2.0 - gap - between - item_width(item)
([step(count - 2), step(count - 1)], [step(1), step(2)])
}
// an item's left edge in the carousel around `open`'s card, `card_w` wide, in the middle, and its side of the card (-1, 0 for the card's own, 1)
fn carousel(item: Item, open: Pod, card_w: f32, middle: f32) -> (f32, i8) {
// the card's own pill, while it fades into the card or out of it, sits in the card's middle
if item == Item::Pod(open) {
return (middle - item_width(item) / 2.0, 0);
}
let (spacing, gap) = (layout::spacing(), layout::gap());
let (left, right) = beside(open);
match left.iter().position(|other| *other == item) {
// leftwards from the card, past the one between
Some(k) => {
let between: f32 = left[k + 1..].iter().map(|other| item_width(*other) + spacing).sum();
(middle - card_w / 2.0 - gap - between - item_width(item), -1)
}
None => {
let k = right.iter().position(|other| *other == item).unwrap_or(0);
let between: f32 = right[..k].iter().map(|other| item_width(*other) + spacing).sum();
(middle + card_w / 2.0 + gap + between, 1)
}
}
}
// an item's side of `open` in the closed row
fn closed_side(item: Item, open: Pod) -> i8 {
(index(item) as i8 - index(Item::Pod(open)) as i8).signum()
}
/*
* from one place to another by `k`, with how much it shows: sliding when it stays on its
* side of the card; changing sides it fades out where it is over the first half and in at
* its new place over the second, so it never crosses the card
*/
fn travel(from: (f32, i8), to: (f32, i8), k: f32) -> (f32, f32) {
if from.1 == to.1 || from.1 == 0 || to.1 == 0 {
(mix(from.0, to.0, k), 1.0)
} else if k < 0.5 {
(from.0, 1.0 - 2.0 * k)
} else {
let between: f32 = ROW[centre + 1..index].iter().map(|other| item_width(*other) + spacing).sum();
middle + card_w / 2.0 + gap + between
(to.0, 2.0 * k - 1.0)
}
}
@@ -329,8 +371,8 @@ struct Scene {
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,
// the row's move into the carousel: it starts only after the hand-over (see holds_island), so the island's pill never shows twice
row_grow: f32,
}
fn scene(state: &Pods, monitor: &Monitor) -> Scene {
@@ -342,25 +384,33 @@ fn scene(state: &Pods, monitor: &Monitor) -> Scene {
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);
let row_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 }
Scene { card, grow, pill_shown, reveal, switch, previous, middle: monitor.width as f32 / 2.0, island_w: island::width_on(here), row_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 {
// an item's left edge on the monitor and how much it shows: between the closed row and the carousel by `row_grow`, between two carousels by `switch`
fn place(&self, item: Item) -> (f32, f32) {
let closed = closed_x(item, self.middle, self.island_w);
let Some((open, _)) = self.card else { return closed };
let Some((open, _)) = self.card else { return (closed, 1.0) };
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,
let now = carousel(item, open, open.size().0, self.middle);
let (open_x, open_shown, open_side) = match self.previous {
Some(before) => {
let from = carousel(item, before, before.size().0, self.middle);
let (x, shown) = travel(from, now, self.switch);
(x, shown, if self.switch < 0.5 { from.1 } else { now.1 })
}
None => (now.0, 1.0, now.1),
};
mix(closed, open_x, if item == Item::Island { self.island_grow } else { self.grow })
let (x, shown) = travel((closed, closed_side(item, open)), (open_x, open_side), self.row_grow);
(x, shown * open_shown)
}
}
@@ -403,7 +453,11 @@ pub fn row_view(monitor: &Monitor) -> LayerWindow {
let mut regions: Vec<InputArea> = Vec::new();
for item in ROW {
let x = scene.place(item);
let (x, seen) = scene.place(item);
if seen <= 0.0 {
continue;
}
match item {
Item::Island => {
@@ -412,10 +466,14 @@ pub fn row_view(monitor: &Monitor) -> LayerWindow {
continue;
}
// it shrinks to its cover (or its bars) as it moves aside
let small = scene.row_grow;
let width = mix(island_pill::width(), island::small_width(), small);
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()));
let pill = island::pill_face(here, small).cursor(Pointer).on_hover(edge::over_bar).on_click(move |_| island_tapped(on.clone()));
let pill = if seen < 1.0 { pill.opacity(seen) } else { pill };
regions.push(InputArea { x: x as i32, y: top as i32, width: island_pill::width().ceil() as i32, height: h.ceil() as i32 });
regions.push(InputArea { x: x as i32, y: top as i32, width: width.ceil() as i32, height: h.ceil() as i32 });
layers.push(Box::new(pill.translate(x, top)));
}
Item::Pod(pod) => {
@@ -427,7 +485,7 @@ pub fn row_view(monitor: &Monitor) -> LayerWindow {
_ if card.is_some_and(|(open, t)| open == pod && t > 2.0 * HAND_OVER) => continue,
Some(before) if before == pod => switch,
_ => 1.0,
};
} * seen;
if shown <= 0.0 {
continue;