feat: implemented disabling animations (#762)
* feat: implemented disabling animations * fix: home page and notification toasts animating weirdly Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
This commit is contained in:
parent
795d80870b
commit
8ca2c30993
14 changed files with 128 additions and 79 deletions
|
|
@ -4,7 +4,7 @@ use std::borrow::Cow;
|
|||
use freya::prelude::*;
|
||||
|
||||
use crate::{
|
||||
components::{Icon, IconType},
|
||||
components::{Icon, IconTint, IconType},
|
||||
theme::colors,
|
||||
};
|
||||
|
||||
|
|
@ -273,6 +273,12 @@ impl Component for Button {
|
|||
palette.disabled_foreground
|
||||
};
|
||||
|
||||
// Child icons would otherwise inherit this through a `styled` event that lands a couple of
|
||||
// frames after the button paints, so hand it straight to them instead
|
||||
let mut icon_tint = use_state(|| foreground);
|
||||
icon_tint.set_if_modified(foreground);
|
||||
use_hook(|| provide_context(IconTint(icon_tint)));
|
||||
|
||||
let border_color = if !enabled() {
|
||||
palette.border
|
||||
} else if pressing() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ use oneclient_content::packages::ProviderId;
|
|||
|
||||
use crate::{AppAssets, theme::colors};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub struct IconTint(pub State<Color>);
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct Icon {
|
||||
pub icon: IconType,
|
||||
|
|
@ -49,12 +52,18 @@ impl Component for Icon {
|
|||
fn render(&self) -> impl IntoElement {
|
||||
let path = self.icon.path();
|
||||
let bytes = use_memo(move || AppAssets::get_bytes(path).unwrap_or_default());
|
||||
let tint = use_hook(try_consume_context::<IconTint>);
|
||||
|
||||
let color = self
|
||||
.color
|
||||
.or_else(|| tint.map(|tint| *tint.0.read()))
|
||||
.unwrap_or_else(colors::fg_primary);
|
||||
|
||||
SvgViewer::new((path, bytes.read().cloned()))
|
||||
.show_loader(false)
|
||||
.width(Size::px(self.size_px))
|
||||
.height(Size::px(self.size_px))
|
||||
.map(self.color, |svg, color| svg.color(color))
|
||||
.color(color)
|
||||
.map(self.rotation, |svg, degrees| svg.rotation(degrees))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ pub use context_menu::ContextMenu;
|
|||
pub use dropdown::Dropdown;
|
||||
pub use dynamic_art::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
pub use file_drop::FileDropOverlay;
|
||||
pub use icons::{Icon, IconType};
|
||||
pub use icons::{Icon, IconTint, IconType};
|
||||
pub use java_install_manager::JavaInstallManager;
|
||||
pub use generic_prompt::GenericPromptOverlay;
|
||||
pub use java_prompt::JavaPromptOverlay;
|
||||
|
|
@ -78,6 +78,6 @@ pub use status_bar::StatusBar;
|
|||
pub use tab_bar::{TabBar, TabItem};
|
||||
pub use text_input::{TextInput, validate_number};
|
||||
pub use toasts::Toasts;
|
||||
pub use toggle::{toggle, toggle_controlled, toggle_disabled};
|
||||
pub use toggle::{toggle, toggle_controlled};
|
||||
pub use update_prompt::UpdatePromptOverlay;
|
||||
pub use version_card::VersionCard;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use oneclient_core::clusters::Cluster;
|
|||
use crate::components::{ART_PREVIEW_EDGE, DynamicArt, Icon, IconType};
|
||||
use crate::hooks::{settled_or_loading, use_active_cluster_id, use_clusters};
|
||||
use crate::routes::Route;
|
||||
use crate::theme;
|
||||
use crate::theme::colors;
|
||||
use crate::ui::{border_all, border_all_color};
|
||||
use crate::utils::sort_clusters_for_home;
|
||||
|
|
@ -26,12 +27,18 @@ pub struct RecentsRow;
|
|||
impl Component for RecentsRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let clusters_query = use_clusters();
|
||||
let mut visible_slots = use_state(|| 1_usize);
|
||||
let platform = Platform::get();
|
||||
let root_size = platform.root_size;
|
||||
let scale_factor = platform.scale_factor;
|
||||
|
||||
let clusters = settled_or_loading(&clusters_query).unwrap_or_default();
|
||||
|
||||
let sorted: Vec<Cluster> = sort_clusters_for_home(clusters);
|
||||
let slots = *visible_slots.read();
|
||||
|
||||
let slots = recent_card_slots_for_window(
|
||||
root_size.read().width,
|
||||
*scale_factor.read() as f32,
|
||||
);
|
||||
|
||||
// Budget off cluster count not slot count an offset-only change makes the layout
|
||||
// engine reuse a card's cached area freezing survivors at pre-resize widths
|
||||
|
|
@ -52,12 +59,6 @@ impl Component for RecentsRow {
|
|||
.width(Size::fill())
|
||||
.height(Size::px(ROW_HEIGHT_PX))
|
||||
.content(Content::Flex)
|
||||
.on_sized(move |event: Event<SizedEventData>| {
|
||||
let next = recent_card_slots_for_width(event.data().area.width());
|
||||
if next != *visible_slots.peek() {
|
||||
*visible_slots.write() = next;
|
||||
}
|
||||
})
|
||||
.horizontal()
|
||||
.spacing(CARD_GAP_PX)
|
||||
.children(display.iter().enumerate().map(|(index, cluster)| {
|
||||
|
|
@ -293,6 +294,15 @@ impl Component for OtherVersionsTile {
|
|||
}
|
||||
}
|
||||
|
||||
fn recent_card_slots_for_window(window_width_px: f32, scale_factor: f32) -> usize {
|
||||
let scale = if scale_factor > 0. && scale_factor.is_finite() {
|
||||
scale_factor
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
recent_card_slots_for_width(window_width_px / scale - theme::HOME_PADDING_PX * 2.)
|
||||
}
|
||||
|
||||
/// `n` cards occupy `n` gaps (n-1 between cards one before the tile) so `n * (MIN + GAP) + MORE`
|
||||
/// must fit
|
||||
/// Always returns at least 1 the card carries no `min_width` and just renders narrower
|
||||
|
|
@ -305,53 +315,3 @@ fn recent_card_slots_for_width(row_width_px: f32) -> usize {
|
|||
let slot = MIN_CARD_WIDTH_PX + CARD_GAP_PX;
|
||||
(available / slot).floor().max(1.0) as usize
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn row_fits(slots: usize, row_width_px: f32) -> bool {
|
||||
let cards = slots as f32 * MIN_CARD_WIDTH_PX;
|
||||
let gaps = slots as f32 * CARD_GAP_PX;
|
||||
cards + gaps + MORE_TILE_WIDTH_PX <= row_width_px
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slots_fill_the_row_without_overflowing_it() {
|
||||
for width in (200..3200).step_by(4) {
|
||||
let width = width as f32;
|
||||
let slots = recent_card_slots_for_width(width);
|
||||
|
||||
assert!(slots >= 1, "{width}px left no room for a card");
|
||||
if slots > 1 {
|
||||
assert!(row_fits(slots, width), "{slots} cards overflow {width}px");
|
||||
}
|
||||
assert!(
|
||||
!row_fits(slots + 1, width),
|
||||
"{width}px had room for {} cards",
|
||||
slots + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_item_settles_when_the_intro_ends() {
|
||||
for clusters in 0..24 {
|
||||
let items = intro_items(clusters);
|
||||
for index in 0..=clusters {
|
||||
assert_eq!(
|
||||
stagger_eased(1.0, index, items),
|
||||
1.0,
|
||||
"item {index} of {clusters} never finished rising"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_intro_runs_the_same_length_whatever_fits_on_screen() {
|
||||
assert_eq!(intro_items(3), intro_items(3));
|
||||
assert_eq!(intro_ms(intro_items(30)), intro_ms(INTRO_ITEMS));
|
||||
assert_eq!(intro_ms(intro_items(0)), CARD_MS);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,15 @@ use crate::{
|
|||
ui::{divider},
|
||||
components::{Button, ButtonSize, Icon, IconType, progress_track},
|
||||
hooks::{use_dispatch, use_notifications_snapshot},
|
||||
motion::use_animations_enabled,
|
||||
notifications::{InboxEntry, NotificationActionKind},
|
||||
theme::colors,
|
||||
};
|
||||
|
||||
const TOAST_W: f32 = 300.;
|
||||
const TRAVEL: f32 = 320.;
|
||||
const TTL_MS: f32 = 5000.;
|
||||
const TTL_MS: f32 = crate::notifications::MESSAGE_TOAST_TTL.as_millis() as f32;
|
||||
const TTL_STEP: std::time::Duration = std::time::Duration::from_millis(100);
|
||||
/// Below the navbar a smaller offset let the startup progress toast cover the nav links
|
||||
const TOP_PX: f32 = crate::theme::NAVBAR_HEIGHT_PX + 12.;
|
||||
|
||||
|
|
@ -126,22 +128,41 @@ impl Component for ToastCard {
|
|||
AnimNum::new(0., 1.).time(260).ease(Ease::Out)
|
||||
});
|
||||
|
||||
let animations = use_animations_enabled();
|
||||
let paused = *self.hovering.read() > 0;
|
||||
let mut was_paused = use_state(|| false);
|
||||
let mut from_pct = use_state(|| 100.);
|
||||
let mut run_start = use_state(Instant::now);
|
||||
let mut wall_pct = use_state(|| 100.);
|
||||
|
||||
if paused != *was_paused.peek() {
|
||||
if paused {
|
||||
let elapsed_pct = run_start.peek().elapsed().as_secs_f32() * 1000. / TTL_MS * 100.;
|
||||
let remaining = *from_pct.peek() - elapsed_pct;
|
||||
from_pct.set(remaining.clamp(0., 100.));
|
||||
let remaining = remaining_pct(*from_pct.peek(), *run_start.peek());
|
||||
from_pct.set(remaining);
|
||||
wall_pct.set(remaining);
|
||||
} else {
|
||||
run_start.set(Instant::now());
|
||||
}
|
||||
was_paused.set(paused);
|
||||
}
|
||||
|
||||
let mut ticker = use_state(|| None::<OwnedTaskHandle>);
|
||||
use_side_effect_with_deps(&(animations, paused), move |&(animations, paused)| {
|
||||
if animations || paused {
|
||||
ticker.set(None);
|
||||
return;
|
||||
}
|
||||
let handle = spawn(async move {
|
||||
let platform = Platform::get();
|
||||
loop {
|
||||
wall_pct.set(remaining_pct(*from_pct.peek(), *run_start.peek()));
|
||||
platform.send(UserEvent::RequestRedraw);
|
||||
tokio::time::sleep(TTL_STEP).await;
|
||||
}
|
||||
});
|
||||
ticker.set(Some(handle.owned()));
|
||||
});
|
||||
|
||||
let ttl_bar_anim =
|
||||
use_animation_with_dependencies(&(paused, *from_pct.peek()), |conf, deps| {
|
||||
let &(paused, from) = deps;
|
||||
|
|
@ -160,9 +181,12 @@ impl Component for ToastCard {
|
|||
|
||||
let offset = slide.read().value();
|
||||
let opacity = fade.read().value();
|
||||
let ttl_pct = ttl_bar_anim.read().value();
|
||||
let ttl_pct = if animations {
|
||||
ttl_bar_anim.read().value()
|
||||
} else {
|
||||
*wall_pct.read()
|
||||
};
|
||||
|
||||
// A card can be unmounted while hovered which emits no `out` event
|
||||
let mut hovering = self.hovering;
|
||||
let hover_flag = use_hook(|| Rc::new(Cell::new(false)));
|
||||
let (over_flag, out_flag, drop_flag) =
|
||||
|
|
@ -217,6 +241,11 @@ impl Component for ToastCard {
|
|||
}
|
||||
}
|
||||
|
||||
fn remaining_pct(from_pct: f32, run_start: Instant) -> f32 {
|
||||
let elapsed_pct = run_start.elapsed().as_secs_f32() * 1000. / TTL_MS * 100.;
|
||||
(from_pct - elapsed_pct).clamp(0., 100.)
|
||||
}
|
||||
|
||||
fn toast_shell(entry: &InboxEntry) -> Rect {
|
||||
let border_color = match entry.level {
|
||||
Level::Error => colors::danger(),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ pub fn toggle(value: State<bool>) -> impl IntoElement {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn toggle_disabled(value: State<bool>) -> impl IntoElement {
|
||||
Switch {
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use crate::components::{
|
|||
};
|
||||
use crate::hooks::{SplashState, use_provide_splash};
|
||||
use crate::layout::HomeArtPrefetch;
|
||||
use crate::motion::AnimationClockDriver;
|
||||
use crate::routes::Route;
|
||||
use crate::theme;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -75,6 +76,7 @@ impl Component for RootLayout {
|
|||
.child(PackageUpdatePopup)
|
||||
.child(StatusBar)
|
||||
.child(SplashCurtain)
|
||||
.child(AnimationClockDriver)
|
||||
.child(HomeArtPrefetch)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ mod launcher;
|
|||
pub mod state;
|
||||
mod transfer;
|
||||
mod layout;
|
||||
mod motion;
|
||||
mod notifications;
|
||||
pub mod platform;
|
||||
pub mod recovery;
|
||||
|
|
|
|||
29
packages/oneclient_app/src/motion.rs
Normal file
29
packages/oneclient_app/src/motion.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use freya::prelude::*;
|
||||
|
||||
use crate::hooks::use_settings_snapshot;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct AnimationClockDriver;
|
||||
|
||||
impl Component for AnimationClockDriver {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let enabled = use_settings_snapshot().settings.animations_enabled;
|
||||
let clock = AnimationClock::get();
|
||||
|
||||
use_side_effect_with_deps(&enabled, move |&enabled| {
|
||||
if enabled {
|
||||
clock.enable();
|
||||
} else {
|
||||
clock.disable();
|
||||
}
|
||||
});
|
||||
|
||||
rect().into_element()
|
||||
}
|
||||
}
|
||||
|
||||
/// Read from the setting rather than the clock so a component re-renders when the preference is
|
||||
/// toggled, and so it never reads the one frame of stale speed before the driver above applies it
|
||||
pub fn use_animations_enabled() -> bool {
|
||||
use_settings_snapshot().settings.animations_enabled
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ use crate::AppAssets;
|
|||
pub mod colors;
|
||||
|
||||
pub const NAVBAR_HEIGHT_PX: f32 = 80.;
|
||||
pub const HOME_PADDING_PX: f32 = 48.;
|
||||
pub const DEFAULT_FONT: &str = "Poppins";
|
||||
pub const MONO_FONT: &str = "JetBrains Mono";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use freya::prelude::*;
|
||||
|
||||
use crate::components::{ActiveClusterPanel, RecentsRow};
|
||||
use crate::theme;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct Home;
|
||||
|
|
@ -13,7 +14,7 @@ impl Component for Home {
|
|||
.height(Size::fill())
|
||||
.content(Content::Flex)
|
||||
.overflow(Overflow::Clip)
|
||||
.padding(48.)
|
||||
.padding(theme::HOME_PADDING_PX)
|
||||
.main_align(Alignment::Center)
|
||||
.child(
|
||||
rect()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use freya::prelude::*;
|
||||
|
||||
use super::settings_page;
|
||||
use crate::components::{Icon, IconType, toggle, toggle_disabled};
|
||||
use crate::components::{Icon, IconType, toggle};
|
||||
use crate::hooks::{use_dispatch, use_settings_snapshot};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
|
@ -50,7 +50,6 @@ pub struct SettingsAppearance;
|
|||
impl Component for SettingsAppearance {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let selected_theme = use_state(|| 0usize);
|
||||
let animations_on = use_state(|| true);
|
||||
|
||||
let settings = use_settings_snapshot().settings;
|
||||
let dispatch = use_dispatch();
|
||||
|
|
@ -60,15 +59,22 @@ impl Component for SettingsAppearance {
|
|||
move || v
|
||||
});
|
||||
|
||||
let animations_on = use_state({
|
||||
let v = settings.animations_enabled;
|
||||
move || v
|
||||
});
|
||||
|
||||
let mut first = use_state(|| true);
|
||||
use_side_effect(move || {
|
||||
let enabled = *dynamic_bg.read();
|
||||
let parallax = *dynamic_bg.read();
|
||||
let animations = *animations_on.read();
|
||||
if *first.peek() {
|
||||
first.set(false);
|
||||
return;
|
||||
}
|
||||
let mut next = settings.clone();
|
||||
next.dynamic_background_enabled = enabled;
|
||||
next.dynamic_background_enabled = parallax;
|
||||
next.animations_enabled = animations;
|
||||
dispatch.set_settings(next);
|
||||
});
|
||||
|
||||
|
|
@ -249,10 +255,10 @@ fn dynamic_background_row(enabled: State<bool>) -> impl IntoElement {
|
|||
}
|
||||
|
||||
fn animations_row(animations_on: State<bool>) -> impl IntoElement {
|
||||
settings_row_disabled(
|
||||
settings_row(
|
||||
IconType::Play,
|
||||
"Animations",
|
||||
"Toggle all animations in the launcher. May cause buggy behavior.",
|
||||
toggle_disabled(animations_on),
|
||||
"Disable all launcher animations and transitions.",
|
||||
toggle(animations_on),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ impl Component for OnboardingPreferences {
|
|||
let mut reduce_motion = use_onboarding_selection().reduce_motion;
|
||||
|
||||
let animations_on = use_state({
|
||||
let v = !*reduce_motion.peek();
|
||||
let v = settings.animations_enabled;
|
||||
move || v
|
||||
});
|
||||
use_side_effect(move || {
|
||||
|
|
@ -32,13 +32,15 @@ impl Component for OnboardingPreferences {
|
|||
|
||||
let mut first = use_state(|| true);
|
||||
use_side_effect(move || {
|
||||
let enabled = *parallax_on.read();
|
||||
let parallax = *parallax_on.read();
|
||||
let animations = *animations_on.read();
|
||||
if *first.peek() {
|
||||
first.set(false);
|
||||
return;
|
||||
}
|
||||
let mut next = settings.clone();
|
||||
next.dynamic_background_enabled = enabled;
|
||||
next.dynamic_background_enabled = parallax;
|
||||
next.animations_enabled = animations;
|
||||
dispatch.set_settings(next);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ pub struct LauncherSettings {
|
|||
pub global_game_settings: GameSettingsProfile,
|
||||
pub allow_parallel_running_clusters: bool,
|
||||
pub dynamic_background_enabled: bool,
|
||||
pub animations_enabled: bool,
|
||||
pub view_states: BTreeMap<String, ViewState>,
|
||||
pub seen_onboarding: bool,
|
||||
pub accepted_tos_version: u32,
|
||||
|
|
@ -68,6 +69,7 @@ impl Default for LauncherSettings {
|
|||
global_game_settings: GameSettingsProfile::default_global_profile(),
|
||||
allow_parallel_running_clusters: false,
|
||||
dynamic_background_enabled: true,
|
||||
animations_enabled: true,
|
||||
view_states: BTreeMap::new(),
|
||||
seen_onboarding: false,
|
||||
accepted_tos_version: 0,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue