130 lines
4.3 KiB
Dart
130 lines
4.3 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();
|
|
}
|
|
|
|
void togglePause() => userPaused = !_userPaused;
|
|
|
|
/// 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;
|
|
}
|
|
} |