Files
app-flutter/lib/widgets/status_icons.dart
T
mikkeli 5d0d2c0007
/ crosscheck (push) Successful in 1s
CI / Analyze & Test (push) Successful in 21s
CI / Debug APK (push) Successful in 40s
Status bar: part lamps (CAN, OBD, IMU, CAM), drawn signal and battery icons
WiFi and LTE show signal in four steps with Android's traffic arrows; the battery is
filled to its level in good (charging), caution (under 40 %) or alarm (under 20 %).
OBD, IMU and CAM are lamps by CAN's rule; CAM reads orin-status' new cam_up and vision_up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:01:40 +09:00

208 lines
5.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The status bar's drawn icons, Android-like: a battery whose fill shows level and
// colour, WiFi and cellular signal in four steps, and the traffic arrow pair. Drawn
// rather than taken from the icon font, which has no four-step cellular icon and no
// separately coloured battery fill.
import 'dart:math' as math;
import 'package:flutter/material.dart';
/// A horizontal battery: [outline] for the case, the inside filled to [level]
/// (0–1, null unknown) in [fill].
class BatteryIcon extends StatelessWidget {
const BatteryIcon({
super.key,
required this.level,
required this.fill,
required this.outline,
});
final double? level;
final Color fill;
final Color outline;
@override
Widget build(BuildContext context) => CustomPaint(
size: const Size(24, 12),
painter: _Battery(level, fill, outline),
);
}
class _Battery extends CustomPainter {
_Battery(this.level, this.fill, this.outline);
final double? level;
final Color fill;
final Color outline;
@override
void paint(Canvas c, Size s) {
const nub = 2.0;
final body = Rect.fromLTWH(0.75, 0.75, s.width - nub - 1.5, s.height - 1.5);
c.drawRRect(
RRect.fromRectAndRadius(body, const Radius.circular(2.5)),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.5
..color = outline,
);
c.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(s.width - nub, s.height * 0.3, nub, s.height * 0.4),
const Radius.circular(1),
),
Paint()..color = outline,
);
final l = level;
if (l == null) return;
final inner = body.deflate(2);
c.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(
inner.left,
inner.top,
inner.width * l.clamp(0, 1),
inner.height,
),
const Radius.circular(1),
),
Paint()..color = fill,
);
}
@override
bool shouldRepaint(_Battery o) =>
o.level != level || o.fill != fill || o.outline != outline;
}
/// Signal in [bars] of four (null: off, drawn crossed), WiFi's fan or cellular's
/// triangle; the unlit part faint.
class SignalIcon extends StatelessWidget {
const SignalIcon.wifi({super.key, required this.bars, required this.color})
: wifi = true;
const SignalIcon.cellular({
super.key,
required this.bars,
required this.color,
}) : wifi = false;
final int? bars;
final Color color;
final bool wifi;
@override
Widget build(BuildContext context) => CustomPaint(
size: const Size(18, 16),
painter: _Signal(bars, color, wifi),
);
}
class _Signal extends CustomPainter {
_Signal(this.bars, this.color, this.wifi);
final int? bars;
final Color color;
final bool wifi;
@override
void paint(Canvas c, Size s) {
final faint = Paint()..color = color.withValues(alpha: 0.3);
final lit = Paint()..color = color;
final n = bars ?? 0;
if (wifi) {
// A quarter-circle fan standing on its point; lit from the point outwards.
final apex = Offset(s.width / 2, s.height);
Path fan(double r) => Path()
..moveTo(apex.dx, apex.dy)
..arcTo(
Rect.fromCircle(center: apex, radius: r),
-3 * math.pi / 4,
math.pi / 2,
false,
)
..close();
final full = s.height;
c.drawPath(fan(full), faint);
if (n > 0) c.drawPath(fan(full * (0.25 + 0.75 * n / 4)), lit);
} else {
// A right triangle rising to the right; lit from the left.
final tri = Path()
..moveTo(0, s.height)
..lineTo(s.width, s.height)
..lineTo(s.width, 0)
..close();
c.drawPath(tri, faint);
if (n > 0) {
c.save();
c.clipRect(
Rect.fromLTWH(0, 0, s.width * (0.2 + 0.8 * n / 4), s.height),
);
c.drawPath(tri, lit);
c.restore();
}
}
if (bars == null) {
// Off: a stroke through it.
c.drawLine(
Offset(s.width * 0.15, s.height * 0.15),
Offset(s.width * 0.85, s.height * 0.95),
Paint()
..color = color
..strokeWidth = 1.5,
);
}
}
@override
bool shouldRepaint(_Signal o) =>
o.bars != bars || o.color != color || o.wifi != wifi;
}
/// Android's traffic pair beside a signal icon: up lit while sending, down while
/// receiving.
class TrafficArrows extends StatelessWidget {
const TrafficArrows({
super.key,
required this.up,
required this.down,
required this.color,
});
final bool up;
final bool down;
final Color color;
@override
Widget build(BuildContext context) =>
CustomPaint(size: const Size(6, 14), painter: _Arrows(up, down, color));
}
class _Arrows extends CustomPainter {
_Arrows(this.up, this.down, this.color);
final bool up;
final bool down;
final Color color;
@override
void paint(Canvas c, Size s) {
Paint p(bool on) =>
Paint()..color = on ? color : color.withValues(alpha: 0.25);
final h = s.height / 2 - 1;
c.drawPath(
Path()
..moveTo(0, h)
..lineTo(s.width, h)
..lineTo(s.width / 2, 0)
..close(),
p(up),
);
c.drawPath(
Path()
..moveTo(0, s.height - h)
..lineTo(s.width, s.height - h)
..lineTo(s.width / 2, s.height)
..close(),
p(down),
);
}
@override
bool shouldRepaint(_Arrows o) =>
o.up != up || o.down != down || o.color != color;
}