Chart background on dark theme

This commit is contained in:
2026-04-22 01:04:04 -03:00
parent 3758e06fde
commit 28fc4b70a5
6 changed files with 216 additions and 51 deletions

View File

@@ -41,6 +41,7 @@ class SessionController {
final ValueNotifier<int> _logTick = ValueNotifier(0);
final ValueNotifier<StatusSnapshot> _snapshot =
ValueNotifier(StatusSnapshot.empty());
final ValueNotifier<bool> cursorEnabled = ValueNotifier(false);
ValueListenable<int> get frameTick => _frameTick;
ValueListenable<int> get logTick => _logTick;
@@ -138,6 +139,7 @@ class SessionController {
_frameTick.dispose();
_logTick.dispose();
_snapshot.dispose();
cursorEnabled.dispose();
await decoder.dispose();
await transport.dispose();
viewState.dispose();

View File

@@ -11,11 +11,15 @@ class ChartPainter extends CustomPainter {
required this.channel,
required this.session,
required this.layout,
required this.isDark,
this.hoverLocalPos,
});
final int channel;
final SessionController session;
final LayoutController layout;
final bool isDark;
final Offset? hoverLocalPos;
@override
void paint(Canvas canvas, Size size) {
@@ -24,8 +28,19 @@ class ChartPainter extends CustomPainter {
final plotH = size.height - padTop - padBottom;
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.
final bgPaint = Paint()..color = const Color(0xFFF7F7F4);
final bgPaint = Paint()..color = bgColor;
canvas.drawRect(
Rect.fromLTWH(padLeft, padTop, plotW, plotH),
bgPaint,
@@ -86,7 +101,7 @@ class ChartPainter extends CustomPainter {
// Hatched gap rectangles.
final hatchPaint = Paint()
..color = const Color(0x55993C1D)
..color = hatchColor
..style = PaintingStyle.fill;
for (final g in gaps.hatchedRanges) {
final x0 = xForCol(g.startPx);
@@ -99,7 +114,7 @@ class ChartPainter extends CustomPainter {
// Polyline.
final linePaint = Paint()
..color = const Color(0xFF185FA5)
..color = lineColor
..strokeWidth = 1.2
..style = PaintingStyle.stroke;
final path = Path();
@@ -132,7 +147,7 @@ class ChartPainter extends CustomPainter {
// Sub-pixel marker lines.
final markerPaint = Paint()
..color = const Color(0xAA993C1D)
..color = markerColor
..strokeWidth = 1;
for (final px in gaps.markerPixels) {
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();
// 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.
_drawText(canvas, '${yMax.toStringAsFixed(2)}',
Offset(padLeft - 4, padTop), align: TextAlign.right);
@@ -157,6 +220,44 @@ class ChartPainter extends CustomPainter {
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,
{TextAlign align = TextAlign.left}) {
final tp = TextPainter(

View File

@@ -19,6 +19,13 @@ class ChartWidget extends StatefulWidget {
class _ChartWidgetState extends State<ChartWidget> {
Offset? _dragStart;
int? _dragStartUs;
final ValueNotifier<Offset?> _hover = ValueNotifier(null);
@override
void dispose() {
_hover.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
@@ -35,29 +42,50 @@ class _ChartWidgetState extends State<ChartWidget> {
_Header(channel: widget.channel),
Expanded(
child: LayoutBuilder(builder: (context, constraints) {
return Listener(
onPointerSignal: (ev) =>
_onPointerSignal(ev, constraints),
child: GestureDetector(
onDoubleTap: () => _resetY(),
onHorizontalDragStart: (d) => _dragStartHandler(d),
onHorizontalDragUpdate: (d) =>
_dragUpdate(d, constraints),
onHorizontalDragEnd: (_) => _dragStart = null,
child: ValueListenableBuilder<int>(
valueListenable: scope.session.frameTick,
builder: (_, __, ___) {
return CustomPaint(
painter: ChartPainter(
channel: widget.channel,
session: scope.session,
layout: scope.layout,
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, constraints),
child: GestureDetector(
onDoubleTap: () => _resetY(),
onHorizontalDragStart: (d) => _dragStartHandler(d),
onHorizontalDragUpdate: (d) =>
_dragUpdate(d, constraints),
onHorizontalDragEnd: (_) => _dragStart = null,
child: ListenableBuilder(
listenable: Listenable.merge([
scope.session.frameTick,
_hover,
]),
builder: (_, __) {
return CustomPaint(
painter: ChartPainter(
channel: widget.channel,
session: scope.session,
layout: scope.layout,
isDark: Theme.of(context).brightness ==
Brightness.dark,
hoverLocalPos:
cursorOn ? _hover.value : null,
),
size: Size.infinite,
);
},
),
size: Size.infinite,
);
},
),
),
),
),
);
},
);
}),
),

View File

@@ -125,10 +125,11 @@ class _LayoutDialogState extends State<LayoutDialog> {
}
Widget _gridPreview() {
final scheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
color: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: GridView.builder(
@@ -148,7 +149,7 @@ class _LayoutDialogState extends State<LayoutDialog> {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey.shade400,
color: scheme.outlineVariant,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(4),
@@ -159,16 +160,22 @@ class _LayoutDialogState extends State<LayoutDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('$cellNumber',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: Colors.grey)),
const Text('empty',
style: TextStyle(
fontSize: 11,
color: Colors.grey,
fontStyle: FontStyle.italic)),
Text(
'$cellNumber',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: scheme.onSurfaceVariant,
),
),
Text(
'empty',
style: TextStyle(
fontSize: 11,
color: scheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
],
),
),
@@ -177,8 +184,8 @@ class _LayoutDialogState extends State<LayoutDialog> {
final cfg = _draft.configFor(ch);
return Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade400),
color: scheme.surface,
border: Border.all(color: scheme.outlineVariant),
borderRadius: BorderRadius.circular(4),
),
child: Padding(
@@ -187,15 +194,23 @@ class _LayoutDialogState extends State<LayoutDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('$cellNumber',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: Colors.grey)),
Text('CH$ch · ${cfg.name}',
style: const TextStyle(
fontSize: 11, fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis),
Text(
'$cellNumber',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: scheme.onSurfaceVariant,
),
),
Text(
'CH$ch · ${cfg.name}',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: scheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
],
),
),

View File

@@ -26,11 +26,11 @@ class MiniLogPanel extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
children: [
Text('Errors & fatals',
Text('Filtered log',
style: TextStyle(
fontWeight: FontWeight.w500, fontSize: 12)),
SizedBox(width: 6),
Text('· filtered view',
Text('· severities from settings',
style: TextStyle(fontSize: 11, color: Colors.grey)),
],
),

View File

@@ -58,6 +58,25 @@ class Toolbar extends StatelessWidget {
icon: const Icon(Icons.fit_screen),
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(),
OutlinedButton.icon(
onPressed: () => showDialog<void>(