use super::*;
pub(crate) const FAST_MODE_RAINBOW_FRAME_INTERVAL: Duration = Duration::from_millis(83);
const SCROLL_COALESCE_MAX_EVENTS: usize = 128;
const SCROLL_COALESCE_MAX_DURATION: Duration = Duration::from_millis(2);
pub(crate) fn prompt_cursor_blink_due(
state: &state::MissionControlState,
last_blink: Instant,
) -> bool {
state.is_prompt_focused()
&& state.prompt_is_empty()
&& last_blink.elapsed() >= PROMPT_CURSOR_BLINK_INTERVAL
}
pub(crate) fn next_ui_timer_timeout(
state: &state::MissionControlState,
last_blink: Instant,
last_footer_git_branch_refresh: Instant,
last_fast_mode_rainbow: Option<Instant>,
now: Instant,
) -> Option<Duration> {
let mut next_due = last_footer_git_branch_refresh + FOOTER_GIT_BRANCH_REFRESH_INTERVAL;
if state.is_prompt_focused() && state.prompt_is_empty() {
next_due = next_due.min(last_blink + PROMPT_CURSOR_BLINK_INTERVAL);
}
if let Some(toast) = &state.toast {
next_due = next_due.min(toast.expires_at());
}
if state.fast_mode_rainbow_active() || state.welcome_animation_active() {
let next_rainbow_due = last_fast_mode_rainbow
.map(|last| last + FAST_MODE_RAINBOW_FRAME_INTERVAL)
.unwrap_or_else(|| now + FAST_MODE_RAINBOW_FRAME_INTERVAL);
next_due = next_due.min(next_rainbow_due);
}
if let Some(timeout) = state.activity_motion_timeout(now) {
next_due = next_due.min(now + timeout);
}
if state.session_active_time.is_active() {
next_due = next_due.min(now + Duration::from_secs(1));
}
Some(next_due.saturating_duration_since(now))
}
pub(crate) fn tick_fast_mode_rainbow(
state: &mut state::MissionControlState,
last_fast_mode_rainbow: &mut Option<Instant>,
now: Instant,
) -> bool {
if !state.fast_mode_rainbow_active() && !state.welcome_animation_active() {
*last_fast_mode_rainbow = None;
return false;
}
let Some(last_frame) = *last_fast_mode_rainbow else {
*last_fast_mode_rainbow = Some(now);
return false;
};
if now.saturating_duration_since(last_frame) < FAST_MODE_RAINBOW_FRAME_INTERVAL {
return false;
}
*last_fast_mode_rainbow = Some(now);
let rainbow_advanced = state.advance_fast_mode_rainbow_phase();
let welcome_advanced = state.welcome_animation_active();
if welcome_advanced {
state.welcome_animation_phase = state.welcome_animation_phase.wrapping_add(1);
}
rainbow_advanced || welcome_advanced
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum RedrawIntent {
#[default]
None,
ScrollOnly,
Immediate,
}
impl RedrawIntent {
pub(crate) fn request_immediate(&mut self) {
*self = Self::Immediate;
}
pub(crate) fn request_scroll(&mut self) {
if *self == Self::None {
*self = Self::ScrollOnly;
}
}
pub(crate) fn clear(&mut self) {
*self = Self::None;
}
}
pub(crate) fn should_draw_frame(intent: RedrawIntent) -> bool {
matches!(intent, RedrawIntent::Immediate | RedrawIntent::ScrollOnly)
}
pub(crate) fn coalesce_mouse_scroll_event(
first: crossterm::event::MouseEvent,
terminal_area: ratatui::layout::Rect,
state: &state::MissionControlState,
bridged_events: &Receiver<TerminalInputEvent>,
pending_events: &mut VecDeque<TerminalInputEvent>,
) -> (crossterm::event::MouseEvent, u16, usize) {
let Some(first_batch) = mouse_scroll_batch(first, terminal_area, state) else {
return (first, state::MOUSE_WHEEL_SCROLL_ROWS, 0);
};
let mut notches = 1u16;
let mut coalesced_events: usize = 0;
let started = Instant::now();
while coalesced_events < SCROLL_COALESCE_MAX_EVENTS
&& started.elapsed() < SCROLL_COALESCE_MAX_DURATION
{
let Some(event) = pending_events
.pop_front()
.or_else(|| bridged_events.try_recv().ok())
else {
break;
};
let TerminalInputEvent::Event(crossterm::event::Event::Mouse(mouse)) = event else {
pending_events.push_front(event);
break;
};
let Some(next_batch) = mouse_scroll_batch(mouse, terminal_area, state) else {
pending_events.push_front(TerminalInputEvent::Event(crossterm::event::Event::Mouse(
mouse,
)));
break;
};
if !first_batch.coalesces_with(next_batch) {
pending_events.push_front(TerminalInputEvent::Event(crossterm::event::Event::Mouse(
mouse,
)));
break;
}
notches = notches.saturating_add(1);
coalesced_events = coalesced_events.saturating_add(1);
}
(
first,
notches.saturating_mul(state::MOUSE_WHEEL_SCROLL_ROWS),
coalesced_events,
)
}
impl DrainResult {
pub(crate) fn merge(&mut self, other: DrainResult) {
let DrainResult {
changed,
run_finished,
run_finished_worker_ids,
model_catalog_finished,
model_catalog_loaded,
fast_mode_persisted,
theme_catalog_loaded,
theme_persisted,
startup_critical_loaded,
usage_loaded,
startup_decorative_loaded,
custom_provider_finished,
session_preview_loaded,
session_switch_loaded,
session_title_updated,
rewind_finished,
export_finished,
model_selection_finished,
compaction_runtime_refresh,
footer_git_branch_loaded,
} = other;
self.changed |= changed;
self.run_finished |= run_finished;
self.run_finished_worker_ids.extend(run_finished_worker_ids);
self.model_catalog_finished |= model_catalog_finished;
self.model_catalog_loaded.extend(model_catalog_loaded);
self.fast_mode_persisted.extend(fast_mode_persisted);
self.theme_catalog_loaded.extend(theme_catalog_loaded);
self.theme_persisted.extend(theme_persisted);
self.startup_critical_loaded.extend(startup_critical_loaded);
self.usage_loaded.extend(usage_loaded);
self.startup_decorative_loaded
.extend(startup_decorative_loaded);
self.custom_provider_finished
.extend(custom_provider_finished);
self.session_preview_loaded.extend(session_preview_loaded);
self.session_switch_loaded.extend(session_switch_loaded);
self.session_title_updated.extend(session_title_updated);
self.rewind_finished.extend(rewind_finished);
self.export_finished.extend(export_finished);
self.model_selection_finished
.extend(model_selection_finished);
self.compaction_runtime_refresh
.extend(compaction_runtime_refresh);
self.footer_git_branch_loaded
.extend(footer_git_branch_loaded);
}
}
#[cfg(test)]
mod profiling {
use super::*;
fn mouse(
kind: crossterm::event::MouseEventKind,
column: u16,
row: u16,
) -> crossterm::event::MouseEvent {
crossterm::event::MouseEvent {
kind,
column,
row,
modifiers: crossterm::event::KeyModifiers::NONE,
}
}
fn scroll_event(
kind: crossterm::event::MouseEventKind,
column: u16,
row: u16,
) -> TerminalInputEvent {
TerminalInputEvent::Event(crossterm::event::Event::Mouse(mouse(kind, column, row)))
}
#[test]
#[ignore = "release-mode scroll burst measurement; run with --release --ignored --nocapture"]
fn scroll_coalescing_burst_measurement() {
let area = ratatui::layout::Rect::new(0, 0, 100, 30);
let state = state::MissionControlState::default();
for queued in [8usize, 128, 1_023] {
let (sender, receiver) = bounded(1_024);
for _ in 0..queued {
sender
.send(scroll_event(
crossterm::event::MouseEventKind::ScrollUp,
1,
4,
))
.unwrap();
}
let mut pending = VecDeque::new();
let started = Instant::now();
let (_, rows, coalesced) = coalesce_mouse_scroll_event(
mouse(crossterm::event::MouseEventKind::ScrollUp, 1, 4),
area,
&state,
&receiver,
&mut pending,
);
eprintln!(
"scroll_coalescing queued={} coalesced={} rows={} elapsed_us={} pending={}",
queued,
coalesced,
rows,
started.elapsed().as_micros(),
pending.len()
);
assert!(coalesced <= queued.min(SCROLL_COALESCE_MAX_EVENTS));
assert_eq!(receiver.len() + pending.len() + coalesced, queued);
}
}
}