Chart background on dark theme
This commit is contained in:
@@ -41,6 +41,7 @@ class SessionController {
|
|||||||
final ValueNotifier<int> _logTick = ValueNotifier(0);
|
final ValueNotifier<int> _logTick = ValueNotifier(0);
|
||||||
final ValueNotifier<StatusSnapshot> _snapshot =
|
final ValueNotifier<StatusSnapshot> _snapshot =
|
||||||
ValueNotifier(StatusSnapshot.empty());
|
ValueNotifier(StatusSnapshot.empty());
|
||||||
|
final ValueNotifier<bool> cursorEnabled = ValueNotifier(false);
|
||||||
|
|
||||||
ValueListenable<int> get frameTick => _frameTick;
|
ValueListenable<int> get frameTick => _frameTick;
|
||||||
ValueListenable<int> get logTick => _logTick;
|
ValueListenable<int> get logTick => _logTick;
|
||||||
@@ -138,6 +139,7 @@ class SessionController {
|
|||||||
_frameTick.dispose();
|
_frameTick.dispose();
|
||||||
_logTick.dispose();
|
_logTick.dispose();
|
||||||
_snapshot.dispose();
|
_snapshot.dispose();
|
||||||
|
cursorEnabled.dispose();
|
||||||
await decoder.dispose();
|
await decoder.dispose();
|
||||||
await transport.dispose();
|
await transport.dispose();
|
||||||
viewState.dispose();
|
viewState.dispose();
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ class ChartPainter extends CustomPainter {
|
|||||||
required this.channel,
|
required this.channel,
|
||||||
required this.session,
|
required this.session,
|
||||||
required this.layout,
|
required this.layout,
|
||||||
|
required this.isDark,
|
||||||
|
this.hoverLocalPos,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int channel;
|
final int channel;
|
||||||
final SessionController session;
|
final SessionController session;
|
||||||
final LayoutController layout;
|
final LayoutController layout;
|
||||||
|
final bool isDark;
|
||||||
|
final Offset? hoverLocalPos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
@@ -24,8 +28,19 @@ class ChartPainter extends CustomPainter {
|
|||||||
final plotH = size.height - padTop - padBottom;
|
final plotH = size.height - padTop - padBottom;
|
||||||
if (plotW <= 0 || plotH <= 0) return;
|
if (plotW <= 0 || plotH <= 0) return;
|
||||||
|
|
||||||
|
// Theme-dependent palette.
|
||||||
|
final bgColor = isDark ? const Color(0xFF1E1F22) : const Color(0xFFF7F7F4);
|
||||||
|
final lineColor =
|
||||||
|
isDark ? const Color(0xFF5EA8FF) : const Color(0xFF185FA5);
|
||||||
|
final hatchColor =
|
||||||
|
isDark ? const Color(0x66D17050) : const Color(0x55993C1D);
|
||||||
|
final markerColor =
|
||||||
|
isDark ? const Color(0xCCD17050) : const Color(0xAA993C1D);
|
||||||
|
final cursorColor =
|
||||||
|
isDark ? const Color(0xCCBBBBBB) : const Color(0xAA444444);
|
||||||
|
|
||||||
// Background.
|
// Background.
|
||||||
final bgPaint = Paint()..color = const Color(0xFFF7F7F4);
|
final bgPaint = Paint()..color = bgColor;
|
||||||
canvas.drawRect(
|
canvas.drawRect(
|
||||||
Rect.fromLTWH(padLeft, padTop, plotW, plotH),
|
Rect.fromLTWH(padLeft, padTop, plotW, plotH),
|
||||||
bgPaint,
|
bgPaint,
|
||||||
@@ -86,7 +101,7 @@ class ChartPainter extends CustomPainter {
|
|||||||
|
|
||||||
// Hatched gap rectangles.
|
// Hatched gap rectangles.
|
||||||
final hatchPaint = Paint()
|
final hatchPaint = Paint()
|
||||||
..color = const Color(0x55993C1D)
|
..color = hatchColor
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
for (final g in gaps.hatchedRanges) {
|
for (final g in gaps.hatchedRanges) {
|
||||||
final x0 = xForCol(g.startPx);
|
final x0 = xForCol(g.startPx);
|
||||||
@@ -99,7 +114,7 @@ class ChartPainter extends CustomPainter {
|
|||||||
|
|
||||||
// Polyline.
|
// Polyline.
|
||||||
final linePaint = Paint()
|
final linePaint = Paint()
|
||||||
..color = const Color(0xFF185FA5)
|
..color = lineColor
|
||||||
..strokeWidth = 1.2
|
..strokeWidth = 1.2
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
final path = Path();
|
final path = Path();
|
||||||
@@ -132,7 +147,7 @@ class ChartPainter extends CustomPainter {
|
|||||||
|
|
||||||
// Sub-pixel marker lines.
|
// Sub-pixel marker lines.
|
||||||
final markerPaint = Paint()
|
final markerPaint = Paint()
|
||||||
..color = const Color(0xAA993C1D)
|
..color = markerColor
|
||||||
..strokeWidth = 1;
|
..strokeWidth = 1;
|
||||||
for (final px in gaps.markerPixels) {
|
for (final px in gaps.markerPixels) {
|
||||||
final x = xForCol(px);
|
final x = xForCol(px);
|
||||||
@@ -143,8 +158,56 @@ class ChartPainter extends CustomPainter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cursor crosshair + data dot (still inside the plot clip).
|
||||||
|
int? hoverCol;
|
||||||
|
double? hoverY;
|
||||||
|
int? hoverTsUs;
|
||||||
|
if (hoverLocalPos != null) {
|
||||||
|
final hx = hoverLocalPos!.dx;
|
||||||
|
final hy = hoverLocalPos!.dy;
|
||||||
|
if (hx >= padLeft &&
|
||||||
|
hx <= padLeft + plotW &&
|
||||||
|
hy >= padTop &&
|
||||||
|
hy <= padTop + plotH) {
|
||||||
|
final col = (hx - padLeft).floor().clamp(0, cols.length - 1);
|
||||||
|
hoverCol = col;
|
||||||
|
hoverTsUs = win.startUs +
|
||||||
|
(((col + 0.5) / plotW) * (win.endUs - win.startUs)).round();
|
||||||
|
final cursorPaint = Paint()
|
||||||
|
..color = cursorColor
|
||||||
|
..strokeWidth = 1;
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(xForCol(col), padTop),
|
||||||
|
Offset(xForCol(col), padTop + plotH),
|
||||||
|
cursorPaint,
|
||||||
|
);
|
||||||
|
if (cols[col].hasData) {
|
||||||
|
hoverY = (cols[col].min + cols[col].max) / 2;
|
||||||
|
final yPx = yForVal(hoverY);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(padLeft, yPx),
|
||||||
|
Offset(padLeft + plotW, yPx),
|
||||||
|
cursorPaint,
|
||||||
|
);
|
||||||
|
canvas.drawCircle(
|
||||||
|
Offset(xForCol(col), yPx),
|
||||||
|
3,
|
||||||
|
Paint()..color = lineColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
|
|
||||||
|
// Cursor readout box (outside the clip so it can overlay labels).
|
||||||
|
if (hoverCol != null) {
|
||||||
|
final tStr = hoverTsUs != null ? _fmtTime(hoverTsUs) : '—';
|
||||||
|
final yStr = hoverY != null ? hoverY.toStringAsFixed(3) : '—';
|
||||||
|
_drawReadout(canvas, 'x=$tStr y=$yStr',
|
||||||
|
Offset(padLeft + 4, padTop + 2));
|
||||||
|
}
|
||||||
|
|
||||||
// Axes labels.
|
// Axes labels.
|
||||||
_drawText(canvas, '${yMax.toStringAsFixed(2)}',
|
_drawText(canvas, '${yMax.toStringAsFixed(2)}',
|
||||||
Offset(padLeft - 4, padTop), align: TextAlign.right);
|
Offset(padLeft - 4, padTop), align: TextAlign.right);
|
||||||
@@ -157,6 +220,44 @@ class ChartPainter extends CustomPainter {
|
|||||||
align: TextAlign.right);
|
align: TextAlign.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawReadout(Canvas c, String s, Offset where) {
|
||||||
|
final textColor =
|
||||||
|
isDark ? const Color(0xFFEEEEEE) : const Color(0xFF222222);
|
||||||
|
final bgColor =
|
||||||
|
isDark ? const Color(0xCC2A2B2E) : const Color(0xCCFFFFFF);
|
||||||
|
final borderColor =
|
||||||
|
isDark ? const Color(0x66FFFFFF) : const Color(0x44000000);
|
||||||
|
final tp = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: s,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 10,
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
final bgRect = Rect.fromLTWH(
|
||||||
|
where.dx,
|
||||||
|
where.dy,
|
||||||
|
tp.width + 8,
|
||||||
|
tp.height + 4,
|
||||||
|
);
|
||||||
|
c.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(bgRect, const Radius.circular(3)),
|
||||||
|
Paint()..color = bgColor,
|
||||||
|
);
|
||||||
|
c.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(bgRect, const Radius.circular(3)),
|
||||||
|
Paint()
|
||||||
|
..color = borderColor
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 0.5,
|
||||||
|
);
|
||||||
|
tp.paint(c, Offset(where.dx + 4, where.dy + 2));
|
||||||
|
}
|
||||||
|
|
||||||
void _drawText(Canvas c, String s, Offset where,
|
void _drawText(Canvas c, String s, Offset where,
|
||||||
{TextAlign align = TextAlign.left}) {
|
{TextAlign align = TextAlign.left}) {
|
||||||
final tp = TextPainter(
|
final tp = TextPainter(
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ class ChartWidget extends StatefulWidget {
|
|||||||
class _ChartWidgetState extends State<ChartWidget> {
|
class _ChartWidgetState extends State<ChartWidget> {
|
||||||
Offset? _dragStart;
|
Offset? _dragStart;
|
||||||
int? _dragStartUs;
|
int? _dragStartUs;
|
||||||
|
final ValueNotifier<Offset?> _hover = ValueNotifier(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_hover.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -35,7 +42,18 @@ class _ChartWidgetState extends State<ChartWidget> {
|
|||||||
_Header(channel: widget.channel),
|
_Header(channel: widget.channel),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
return Listener(
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: scope.session.cursorEnabled,
|
||||||
|
builder: (_, cursorOn, __) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: cursorOn
|
||||||
|
? SystemMouseCursors.precise
|
||||||
|
: MouseCursor.defer,
|
||||||
|
onHover: cursorOn
|
||||||
|
? (e) => _hover.value = e.localPosition
|
||||||
|
: null,
|
||||||
|
onExit: cursorOn ? (_) => _hover.value = null : null,
|
||||||
|
child: Listener(
|
||||||
onPointerSignal: (ev) =>
|
onPointerSignal: (ev) =>
|
||||||
_onPointerSignal(ev, constraints),
|
_onPointerSignal(ev, constraints),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -44,20 +62,30 @@ class _ChartWidgetState extends State<ChartWidget> {
|
|||||||
onHorizontalDragUpdate: (d) =>
|
onHorizontalDragUpdate: (d) =>
|
||||||
_dragUpdate(d, constraints),
|
_dragUpdate(d, constraints),
|
||||||
onHorizontalDragEnd: (_) => _dragStart = null,
|
onHorizontalDragEnd: (_) => _dragStart = null,
|
||||||
child: ValueListenableBuilder<int>(
|
child: ListenableBuilder(
|
||||||
valueListenable: scope.session.frameTick,
|
listenable: Listenable.merge([
|
||||||
builder: (_, __, ___) {
|
scope.session.frameTick,
|
||||||
|
_hover,
|
||||||
|
]),
|
||||||
|
builder: (_, __) {
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: ChartPainter(
|
painter: ChartPainter(
|
||||||
channel: widget.channel,
|
channel: widget.channel,
|
||||||
session: scope.session,
|
session: scope.session,
|
||||||
layout: scope.layout,
|
layout: scope.layout,
|
||||||
|
isDark: Theme.of(context).brightness ==
|
||||||
|
Brightness.dark,
|
||||||
|
hoverLocalPos:
|
||||||
|
cursorOn ? _hover.value : null,
|
||||||
),
|
),
|
||||||
size: Size.infinite,
|
size: Size.infinite,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -125,10 +125,11 @@ class _LayoutDialogState extends State<LayoutDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _gridPreview() {
|
Widget _gridPreview() {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade100,
|
color: scheme.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
@@ -148,7 +149,7 @@ class _LayoutDialogState extends State<LayoutDialog> {
|
|||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.grey.shade400,
|
color: scheme.outlineVariant,
|
||||||
style: BorderStyle.solid,
|
style: BorderStyle.solid,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
@@ -159,16 +160,22 @@ class _LayoutDialogState extends State<LayoutDialog> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text('$cellNumber',
|
Text(
|
||||||
style: const TextStyle(
|
'$cellNumber',
|
||||||
|
style: TextStyle(
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.grey)),
|
color: scheme.onSurfaceVariant,
|
||||||
const Text('empty',
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'empty',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey,
|
color: scheme.onSurfaceVariant,
|
||||||
fontStyle: FontStyle.italic)),
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -177,8 +184,8 @@ class _LayoutDialogState extends State<LayoutDialog> {
|
|||||||
final cfg = _draft.configFor(ch);
|
final cfg = _draft.configFor(ch);
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: scheme.surface,
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
border: Border.all(color: scheme.outlineVariant),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -187,15 +194,23 @@ class _LayoutDialogState extends State<LayoutDialog> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text('$cellNumber',
|
Text(
|
||||||
style: const TextStyle(
|
'$cellNumber',
|
||||||
|
style: TextStyle(
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.grey)),
|
color: scheme.onSurfaceVariant,
|
||||||
Text('CH$ch · ${cfg.name}',
|
),
|
||||||
style: const TextStyle(
|
),
|
||||||
fontSize: 11, fontWeight: FontWeight.w500),
|
Text(
|
||||||
overflow: TextOverflow.ellipsis),
|
'CH$ch · ${cfg.name}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: scheme.onSurface,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ class MiniLogPanel extends StatelessWidget {
|
|||||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text('Errors & fatals',
|
Text('Filtered log',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500, fontSize: 12)),
|
fontWeight: FontWeight.w500, fontSize: 12)),
|
||||||
SizedBox(width: 6),
|
SizedBox(width: 6),
|
||||||
Text('· filtered view',
|
Text('· severities from settings',
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey)),
|
style: TextStyle(fontSize: 11, color: Colors.grey)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -58,6 +58,25 @@ class Toolbar extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.fit_screen),
|
icon: const Icon(Icons.fit_screen),
|
||||||
label: const Text('Reset view'),
|
label: const Text('Reset view'),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: scope.session.cursorEnabled,
|
||||||
|
builder: (_, on, __) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
return OutlinedButton.icon(
|
||||||
|
onPressed: () =>
|
||||||
|
scope.session.cursorEnabled.value = !on,
|
||||||
|
icon: const Icon(Icons.my_location),
|
||||||
|
label: const Text('Cursor'),
|
||||||
|
style: on
|
||||||
|
? OutlinedButton.styleFrom(
|
||||||
|
backgroundColor: scheme.primaryContainer,
|
||||||
|
foregroundColor: scheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const _Sep(),
|
const _Sep(),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () => showDialog<void>(
|
onPressed: () => showDialog<void>(
|
||||||
|
|||||||
Reference in New Issue
Block a user