fix: revert draw gating; replace frame-count scroll with wall-clock timer
- Revert src/main.rs to draw-at-top-of-loop (original structure) so auto-scrolling is no longer coupled to event rate. Lock is released before blocking on recv() so the network task is never starved. - Replace ticks_until_scroll counter in on_ui_scroll_tick() with an Instant + Duration (default 100ms). Scroll now advances at a fixed wall-clock rate (10 chars/sec) regardless of draw rate or tick rate, which is the correct long-term fix for the animation regression. - Update tests: assert scroll_interval == 100ms in default/new tests; rewrite test_on_ui_scroll_tick to cover zero-interval (always fires) and long-interval (suppressed) cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C3FpbniZme9mJqgFkMVowz
This commit is contained in:
+12
-10
@@ -5,6 +5,7 @@ mod tests {
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use serde_json::Value;
|
||||
use serial_test::serial;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
|
||||
@@ -80,7 +81,7 @@ mod tests {
|
||||
assert_eq!(app.server_tabs.index, 0);
|
||||
assert_eq!(app.server_tabs.tabs, expected_tab_routes);
|
||||
assert_eq!(app.tick_until_poll, 400);
|
||||
assert_eq!(app.ticks_until_scroll, 64);
|
||||
assert_eq!(app.scroll_interval, Duration::from_millis(100));
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert!(!app.is_loading);
|
||||
@@ -101,7 +102,7 @@ mod tests {
|
||||
assert_eq!(app.error, HorizontallyScrollableText::default());
|
||||
assert_eq!(app.server_tabs.index, 0);
|
||||
assert_eq!(app.tick_until_poll, 400);
|
||||
assert_eq!(app.ticks_until_scroll, 64);
|
||||
assert_eq!(app.scroll_interval, Duration::from_millis(100));
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.is_routing);
|
||||
@@ -247,22 +248,23 @@ mod tests {
|
||||
#[test]
|
||||
fn test_on_ui_scroll_tick() {
|
||||
let mut app = App {
|
||||
ticks_until_scroll: 1,
|
||||
scroll_interval: Duration::ZERO,
|
||||
..App::default()
|
||||
};
|
||||
|
||||
// Zero interval: elapsed() >= scroll_interval is always true, so every
|
||||
// call should signal "scroll now" (ui_scroll_tick_count == 0)
|
||||
app.on_ui_scroll_tick();
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
app.on_ui_scroll_tick();
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
|
||||
// Long interval with a fresh timestamp: should signal "not yet" (count == 1)
|
||||
app.scroll_interval = Duration::from_secs(60);
|
||||
app.last_scroll = Instant::now();
|
||||
app.on_ui_scroll_tick();
|
||||
assert_eq!(app.ui_scroll_tick_count, 1);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
app.on_ui_scroll_tick();
|
||||
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
+8
-4
@@ -7,6 +7,7 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fs, process};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -41,7 +42,8 @@ pub struct App<'a> {
|
||||
pub error: HorizontallyScrollableText,
|
||||
pub notification: Option<Notification>,
|
||||
pub tick_until_poll: u64,
|
||||
pub ticks_until_scroll: u64,
|
||||
pub scroll_interval: Duration,
|
||||
pub last_scroll: Instant,
|
||||
pub tick_count: u64,
|
||||
pub ui_scroll_tick_count: u64,
|
||||
pub is_routing: bool,
|
||||
@@ -171,10 +173,11 @@ impl App<'_> {
|
||||
}
|
||||
|
||||
pub fn on_ui_scroll_tick(&mut self) {
|
||||
if self.ui_scroll_tick_count == self.ticks_until_scroll {
|
||||
if self.last_scroll.elapsed() >= self.scroll_interval {
|
||||
self.ui_scroll_tick_count = 0;
|
||||
self.last_scroll = Instant::now();
|
||||
} else {
|
||||
self.ui_scroll_tick_count += 1;
|
||||
self.ui_scroll_tick_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +263,8 @@ impl Default for App<'_> {
|
||||
is_first_render: true,
|
||||
server_tabs: TabState::new(Vec::new()),
|
||||
tick_until_poll: 400,
|
||||
ticks_until_scroll: 64,
|
||||
scroll_interval: Duration::from_millis(100),
|
||||
last_scroll: Instant::now(),
|
||||
tick_count: 0,
|
||||
ui_scroll_tick_count: 0,
|
||||
is_loading: false,
|
||||
|
||||
+5
-7
@@ -276,12 +276,12 @@ async fn start_ui(
|
||||
|
||||
let input_events = Events::new();
|
||||
|
||||
{
|
||||
let mut app = app.lock().await;
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
}
|
||||
|
||||
loop {
|
||||
{
|
||||
let mut app = app.lock().await;
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
}
|
||||
|
||||
match input_events.next()? {
|
||||
Some(InputEvent::KeyEvent(key)) => {
|
||||
let mut app = app.lock().await;
|
||||
@@ -291,13 +291,11 @@ async fn start_ui(
|
||||
}
|
||||
|
||||
handlers::handle_events(key, &mut app);
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
}
|
||||
|
||||
Some(InputEvent::Tick) => {
|
||||
let mut app = app.lock().await;
|
||||
app.on_tick().await;
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user