Files
TelemetryMonitor/lib/session/view_state.dart

143 lines
4.8 KiB
Dart

import 'package:flutter/foundation.dart';
enum ViewAnchorMode { followLive, absolute }
enum PauseSource { none, user, proto, both }
/// Window state for all charts. X-axis is shared globally.
///
/// Does NOT hold proto-derived pause state — that comes from the per-frame
/// status snapshot (see SessionController). Composing the two flags happens
/// in SessionController.isPaused.
class ViewState extends ChangeNotifier {
ViewState({
Duration windowWidth = const Duration(seconds: 10),
Duration minWindow = const Duration(milliseconds: 10),
}) : _windowWidth = windowWidth,
_minWindow = minWindow;
Duration _windowWidth;
final Duration _minWindow;
ViewAnchorMode _anchorMode = ViewAnchorMode.followLive;
int _absoluteStartUs = 0;
bool _userPaused = false;
Duration get windowWidth => _windowWidth;
ViewAnchorMode get anchorMode => _anchorMode;
int get absoluteStartUs => _absoluteStartUs;
bool get userPaused => _userPaused;
Duration get minWindow => _minWindow;
set userPaused(bool v) {
if (_userPaused == v) return;
_userPaused = v;
notifyListeners();
}
/// Toggle the user pause flag AND snap the X window accordingly: pausing
/// freezes the view at `newestUs - width` (absolute anchor); resuming
/// returns to followLive.
void togglePause({required int newestUs}) {
if (_userPaused) {
_userPaused = false;
_anchorMode = ViewAnchorMode.followLive;
} else {
_userPaused = true;
_absoluteStartUs = newestUs - _windowWidth.inMicroseconds;
_anchorMode = ViewAnchorMode.absolute;
}
notifyListeners();
}
/// Compute the visible time range given current state.
///
/// `nowUs` is wall-clock (or last-packet-time, depending on caller choice).
/// `oldestUs` and `newestUs` bound the buffer; the window is clamped so it
/// cannot extend earlier than `oldestUs`.
({int startUs, int endUs}) currentWindow({
required int nowUs,
required int oldestUs,
required int newestUs,
}) {
final widthUs = _windowWidth.inMicroseconds;
int startUs, endUs;
if (_anchorMode == ViewAnchorMode.followLive) {
endUs = nowUs;
startUs = endUs - widthUs;
} else {
startUs = _absoluteStartUs;
endUs = startUs + widthUs;
}
// Clamp left edge to buffer start.
if (startUs < oldestUs) {
final shift = oldestUs - startUs;
startUs += shift;
endUs += shift;
}
return (startUs: startUs, endUs: endUs);
}
/// Set the window width, clamping to [minWindow, maxWindow].
void setWindowWidth(Duration width, {required Duration maxWindow}) {
var clamped = width;
if (clamped < _minWindow) clamped = _minWindow;
if (clamped > maxWindow) clamped = maxWindow;
if (clamped == _windowWidth) return;
_windowWidth = clamped;
notifyListeners();
}
/// Zoom centered on a pivot timestamp. Factor < 1 zooms in.
void zoomAt({
required int pivotUs,
required double factor,
required Duration maxWindow,
required int oldestUs,
required int newestUs,
}) {
final newWidthUs = (_windowWidth.inMicroseconds * factor).round();
final newWidth = Duration(microseconds: newWidthUs);
setWindowWidth(newWidth, maxWindow: maxWindow);
if (_anchorMode == ViewAnchorMode.absolute) {
// Keep pivot at the same screen position.
final width = _windowWidth.inMicroseconds;
final fraction = (pivotUs - _absoluteStartUs) /
(newWidthUs / factor); // old width
_absoluteStartUs = (pivotUs - (fraction * width)).round();
_clampAnchor(oldestUs: oldestUs, newestUs: newestUs);
}
// In followLive mode the pivot is always near `now`; nothing to adjust.
notifyListeners();
}
/// Pan by a delta. Switches to absolute anchor if not already.
void panBy(Duration delta, {required int oldestUs, required int newestUs}) {
if (_anchorMode == ViewAnchorMode.followLive) {
// Snap to current window position before entering scrollback.
_absoluteStartUs = newestUs - _windowWidth.inMicroseconds;
_anchorMode = ViewAnchorMode.absolute;
}
_absoluteStartUs += delta.inMicroseconds;
_clampAnchor(oldestUs: oldestUs, newestUs: newestUs);
notifyListeners();
}
void goLive() {
if (_anchorMode == ViewAnchorMode.followLive) return;
_anchorMode = ViewAnchorMode.followLive;
notifyListeners();
}
void resetView({Duration defaultWidth = const Duration(seconds: 10)}) {
_anchorMode = ViewAnchorMode.followLive;
_windowWidth = defaultWidth;
notifyListeners();
}
void _clampAnchor({required int oldestUs, required int newestUs}) {
final width = _windowWidth.inMicroseconds;
final maxStart = newestUs - width;
if (_absoluteStartUs > maxStart) _absoluteStartUs = maxStart;
if (_absoluteStartUs < oldestUs) _absoluteStartUs = oldestUs;
}
}