212f8e16d2
- Added WindowDragController to manage custom dragging of sub-windows. - Integrated dragging logic into EqWindow and PlWindow, allowing for smooth window movement. - Introduced docking logic to snap windows together and maintain their positions relative to each other. - Implemented persistence of window positions using SharedPreferences. - Enhanced AppDelegate to support screen bounds and mouse location retrieval for accurate window positioning. - Updated pubspec.yaml to include screen_retriever dependency. - Added comprehensive tests for dock controller and dock logic to ensure correct behavior during window interactions. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
1042 lines
28 KiB
Dart
1042 lines
28 KiB
Dart
import "dart:async";
|
||
import "dart:ui" as ui;
|
||
|
||
import "package:dartamp_skin/dartamp_skin.dart";
|
||
import "package:flutter/material.dart";
|
||
|
||
import "../common_widgets.dart";
|
||
import "../player/player_state.dart";
|
||
import "../skin/skin_cursors.dart";
|
||
import "../windows/sub_window_channel.dart";
|
||
|
||
const Size _plMainSize = Size(280, 290);
|
||
const Size _plShadeSize = Size(280, 14);
|
||
|
||
const double _plWidth = 280;
|
||
const double _plShadeHeight = 14;
|
||
const double _listLeft = 12;
|
||
const double _listRight = 260;
|
||
|
||
/// Visible rows for a 290-tall window: `(290 - 58) / pe_fontheight(13)`.
|
||
const int _numSongs = 17;
|
||
|
||
/// Row stride (`pe_fontheight` = 13 for the base-skin Arial 12).
|
||
const double _rowStride = 13;
|
||
|
||
/// First row's top in song-list-local coordinates (absolute 22 - 20).
|
||
const double _rowsLocalTop = 2;
|
||
|
||
/// Vertically centers the 6 px bitmap glyphs in the 13 px line.
|
||
const double _rowGlyphOffset = 4;
|
||
|
||
/// The green of the `text.bmp` digital font (`mfont_fgcolor`).
|
||
const Color _displayGreen = Color(0xFF00E200);
|
||
|
||
/// One playlist entry.
|
||
class _Track {
|
||
const _Track(this.name, this.seconds);
|
||
|
||
final String name;
|
||
final int seconds;
|
||
}
|
||
|
||
/// Stand-in playlist (25 tracks so the vertical scrollbar has range: 25 − 17).
|
||
const List<_Track> _playlist = [
|
||
_Track("01. In Your Eyes - Peter Gabriel", 335),
|
||
_Track("02. Sledgehammer - Peter Gabriel", 312),
|
||
_Track("03. Steam - Peter Gabriel", 384),
|
||
_Track("04. Games Without Frontiers - Peter Gabriel", 275),
|
||
_Track("05. Shock the Monkey - Peter Gabriel", 344),
|
||
_Track("06. Red Rain - Peter Gabriel", 322),
|
||
_Track("07. Digging in the Dirt - Peter Gabriel", 353),
|
||
_Track("08. Big Time - Peter Gabriel", 294),
|
||
_Track("09. Don't Give Up - Peter Gabriel", 402),
|
||
_Track("10. Solsbury Hill - Peter Gabriel", 262),
|
||
_Track("11. Biko - Peter Gabriel", 435),
|
||
_Track("12. Darkness - Peter Gabriel", 381),
|
||
_Track("13. Mercy Street - Peter Gabriel", 379),
|
||
_Track("14. San Jacinto - Peter Gabriel", 421),
|
||
_Track("15. No Self Control - Peter Gabriel", 296),
|
||
_Track("16. Wallflower - Peter Gabriel", 442),
|
||
_Track("17. Here Comes the Flood - Peter Gabriel", 303),
|
||
_Track("18. I Have the Touch - Peter Gabriel", 315),
|
||
_Track("19. Family Snapshot - Peter Gabriel", 285),
|
||
_Track("20. Intruder - Peter Gabriel", 294),
|
||
_Track("21. I Don't Remember - Peter Gabriel", 264),
|
||
_Track("22. Mother of Violence - Peter Gabriel", 298),
|
||
_Track("23. Lead a Normal Life - Peter Gabriel", 255),
|
||
_Track("24. Humdrum - Peter Gabriel", 203),
|
||
_Track("25. On the Air - Peter Gabriel", 266),
|
||
];
|
||
|
||
String _fmtTime(int seconds) {
|
||
final m = seconds ~/ 60;
|
||
final s = seconds % 60;
|
||
if (seconds >= 3600) {
|
||
final h = seconds ~/ 3600;
|
||
return "$h:${m.toString().padLeft(2, "0")}:${s.toString().padLeft(2, "0")}";
|
||
}
|
||
return "$m:${s.toString().padLeft(2, "0")}";
|
||
}
|
||
|
||
/// Draws `text.bmp` glyphs with the background chroma-keyed out and tinted to
|
||
/// a target color, so the green digital font can render as white/yellow/etc.
|
||
class _KeyedFont {
|
||
_KeyedFont(this.font);
|
||
|
||
final TextFont font;
|
||
final Map<Color, Paint> _paints = {};
|
||
|
||
Paint _paintFor(Color color) {
|
||
return _paints.putIfAbsent(color, () {
|
||
return Paint()
|
||
..filterQuality = FilterQuality.none
|
||
..colorFilter = ColorFilter.matrix(<double>[
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
color.r * 255,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
color.g * 255,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
color.b * 255,
|
||
-1.5,
|
||
3.0,
|
||
-1.5,
|
||
0,
|
||
-1,
|
||
]);
|
||
});
|
||
}
|
||
|
||
/// Draws [text] left-to-right at ([x], [y]); `clipWidth` clips the line.
|
||
void draw(
|
||
Canvas canvas,
|
||
String text,
|
||
double x,
|
||
double y,
|
||
Color color, {
|
||
double? clipWidth,
|
||
}) {
|
||
if (text.isEmpty) return;
|
||
final paint = _paintFor(color);
|
||
canvas.save();
|
||
if (clipWidth != null) {
|
||
canvas.clipRect(Rect.fromLTWH(x, y, clipWidth, TextFont.charHeight));
|
||
}
|
||
var xp = x;
|
||
for (final ch in text.characters) {
|
||
canvas.drawImageRect(
|
||
font.image,
|
||
font.glyphRect(ch),
|
||
Rect.fromLTWH(xp, y, TextFont.charWidth, TextFont.charHeight),
|
||
paint,
|
||
);
|
||
xp += TextFont.charWidth;
|
||
}
|
||
canvas.restore();
|
||
}
|
||
}
|
||
|
||
/// The Playlist window, rendered directly from the skin bitmaps.
|
||
///
|
||
/// Lives in its own OS window (its own Flutter engine); the parent
|
||
/// [SubWindowRoot] manages native window sizing and cross-window messaging.
|
||
class PlWindow extends StatelessWidget {
|
||
const PlWindow({
|
||
required this.player,
|
||
this.skinFuture,
|
||
this.dragController,
|
||
super.key,
|
||
});
|
||
|
||
final PlayerState player;
|
||
final Future<ClassicSkin>? skinFuture;
|
||
final WindowDragController? dragController;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return FutureBuilder<ClassicSkin>(
|
||
future: skinFuture ?? loadBaseSkin(),
|
||
builder: (context, snapshot) {
|
||
if (snapshot.hasError) {
|
||
return ColoredBox(
|
||
color: const Color(0xFF00FF00),
|
||
child: Center(child: Text("Skin load failed: ${snapshot.error}")),
|
||
);
|
||
}
|
||
if (!snapshot.hasData) {
|
||
return const ColoredBox(color: Colors.black);
|
||
}
|
||
return SkinCursorsGate(
|
||
skin: snapshot.data!,
|
||
child: WindowDragSurface(
|
||
dragController: dragController,
|
||
onWindowShadeToggle: player.togglePlShade,
|
||
child: _PlWindowView(skin: snapshot.data!, player: player),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PlWindowView extends StatelessWidget {
|
||
const _PlWindowView({required this.skin, required this.player});
|
||
|
||
final ClassicSkin skin;
|
||
final PlayerState player;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return AnimatedBuilder(
|
||
animation: player,
|
||
builder: (context, _) {
|
||
final size = player.plShade ? _plShadeSize : _plMainSize;
|
||
return Stack(
|
||
clipBehavior: Clip.none,
|
||
children: [
|
||
Positioned(
|
||
left: 0,
|
||
top: 0,
|
||
child: SizedBox.fromSize(
|
||
size: size,
|
||
child: player.plShade
|
||
? _PlShadeLayout(skin: skin, player: player)
|
||
: _PlMainLayout(skin: skin, player: player),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
/// The full 280×290 Playlist window (normal mode).
|
||
class _PlMainLayout extends StatefulWidget {
|
||
const _PlMainLayout({required this.skin, required this.player});
|
||
|
||
final ClassicSkin skin;
|
||
final PlayerState player;
|
||
|
||
@override
|
||
State<_PlMainLayout> createState() => _PlMainLayoutState();
|
||
}
|
||
|
||
class _PlMainLayoutState extends State<_PlMainLayout> {
|
||
int _offset = 0;
|
||
int _selected = -1;
|
||
bool _dragging = false;
|
||
_KeyedFont? _keyedFont;
|
||
|
||
_KeyedFont get _keyed {
|
||
return _keyedFont ??= _KeyedFont(TextFont(widget.skin.image("text.bmp")!));
|
||
}
|
||
|
||
void _onTapDown(TapDownDetails details) {
|
||
if (details.localPosition.dx >= _listRight) return;
|
||
final row = ((details.localPosition.dy - _rowsLocalTop) / _rowStride)
|
||
.floor();
|
||
final absIndex = _offset + row;
|
||
if (row >= 0 && row < _numSongs && absIndex < _playlist.length) {
|
||
setState(() => _selected = absIndex);
|
||
}
|
||
}
|
||
|
||
void _onDragStart(DragStartDetails details) {
|
||
if (details.localPosition.dx >= _listRight) {
|
||
_dragging = true;
|
||
}
|
||
}
|
||
|
||
void _onDragUpdate(DragUpdateDetails details) {
|
||
if (!_dragging) return;
|
||
final t = _playlist.length - _numSongs;
|
||
final pos = ((details.localPosition.dy / 214) * t)
|
||
.round()
|
||
.clamp(0, t)
|
||
.toInt();
|
||
if (pos != _offset) setState(() => _offset = pos);
|
||
}
|
||
|
||
void _onDragEnd(DragEndDetails details) => _dragging = false;
|
||
|
||
void _onDragCancel() => _dragging = false;
|
||
|
||
String _infoString() {
|
||
final selTime = _selected >= 0 ? _playlist[_selected].seconds : 0;
|
||
final totalTime = _playlist.fold<int>(0, (sum, t) => sum + t.seconds);
|
||
return "${_fmtTime(selTime)}/${_fmtTime(totalTime)}";
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final skin = widget.skin;
|
||
final player = widget.player;
|
||
final pledit = skin.image("pledit.bmp")!;
|
||
final elapsedSec = player.position * player.durationSec ~/ 256;
|
||
return Stack(
|
||
children: [
|
||
const Positioned.fill(
|
||
child: ColoredBox(color: Color(0xFF000000)),
|
||
),
|
||
Positioned(
|
||
top: 0,
|
||
left: 0,
|
||
child: CustomPaint(
|
||
size: const Size(_plWidth, 20),
|
||
painter: _PlTitlebarPainter(image: pledit, active: true),
|
||
),
|
||
),
|
||
Positioned(
|
||
top: 20,
|
||
left: 0,
|
||
child: CustomPaint(
|
||
size: const Size(_plWidth, 232),
|
||
painter: _PlBodyPainter(pledit),
|
||
),
|
||
),
|
||
Positioned(
|
||
top: 20,
|
||
left: 0,
|
||
child: GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTapDown: _onTapDown,
|
||
onVerticalDragStart: _onDragStart,
|
||
onVerticalDragUpdate: _onDragUpdate,
|
||
onVerticalDragEnd: _onDragEnd,
|
||
onVerticalDragCancel: _onDragCancel,
|
||
child: CustomPaint(
|
||
size: const Size(_plWidth, 232),
|
||
painter: _PlSongListPainter(
|
||
skin: skin,
|
||
keyed: _keyed,
|
||
offset: _offset,
|
||
selected: _selected,
|
||
dragging: _dragging,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Positioned(
|
||
top: 252,
|
||
left: 0,
|
||
child: CustomPaint(
|
||
size: const Size(_plWidth, 38),
|
||
painter: _PlBottomPainter(pledit),
|
||
),
|
||
),
|
||
_PlButtons(pledit: pledit),
|
||
Positioned(
|
||
top: 262,
|
||
left: 137,
|
||
child: CustomPaint(
|
||
size: const Size(90, 6),
|
||
painter: _PlInfoPainter(keyed: _keyed, text: _infoString()),
|
||
),
|
||
),
|
||
Positioned(
|
||
top: 275,
|
||
left: 194,
|
||
child: CustomPaint(
|
||
size: const Size(32, 6),
|
||
painter: _PlTimePainter(
|
||
keyed: _keyed,
|
||
minutes: elapsedSec ~/ 60,
|
||
seconds: elapsedSec % 60,
|
||
),
|
||
),
|
||
),
|
||
_PlTitleButtons(pledit: pledit, player: player),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// The 20 px titlebar strip (`draw_pe_tbar`), tiling `pledit.bmp` row 0/21.
|
||
class _PlTitlebarPainter extends CustomPainter {
|
||
const _PlTitlebarPainter({required this.image, required this.active});
|
||
|
||
final ui.Image image;
|
||
final bool active;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final paint = Paint()..filterQuality = FilterQuality.none;
|
||
final state = active ? 0.0 : 21.0;
|
||
final w = size.width;
|
||
var xp = 0.0;
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(0, state, 25, 20),
|
||
Rect.fromLTWH(xp, 0, 25, 20),
|
||
paint,
|
||
);
|
||
xp += 25;
|
||
var nt = ((w - 150) / 25).floor();
|
||
if ((nt & 1) != 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(127, state, 12, 20),
|
||
Rect.fromLTWH(xp, 0, 12, 20),
|
||
paint,
|
||
);
|
||
xp += 12;
|
||
}
|
||
nt ~/= 2;
|
||
while (nt > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(127, state, 25, 20),
|
||
Rect.fromLTWH(xp, 0, 25, 20),
|
||
paint,
|
||
);
|
||
xp += 25;
|
||
nt--;
|
||
}
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(26, state, 100, 20),
|
||
Rect.fromLTWH(xp, 0, 100, 20),
|
||
paint,
|
||
);
|
||
xp += 100;
|
||
nt = ((w - 150) / 25).floor();
|
||
if ((nt & 1) != 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(127, state, 13, 20),
|
||
Rect.fromLTWH(xp, 0, 13, 20),
|
||
paint,
|
||
);
|
||
xp += 13;
|
||
}
|
||
nt ~/= 2;
|
||
while (nt > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(127, state, 25, 20),
|
||
Rect.fromLTWH(xp, 0, 25, 20),
|
||
paint,
|
||
);
|
||
xp += 25;
|
||
nt--;
|
||
}
|
||
final rem = (w - 150) % 25;
|
||
if (rem > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(127, state, 25, 20),
|
||
Rect.fromLTWH(xp, 0, rem, 20),
|
||
paint,
|
||
);
|
||
xp += rem;
|
||
}
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(153, state, 25, 20),
|
||
Rect.fromLTWH(xp, 0, 25, 20),
|
||
paint,
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_PlTitlebarPainter oldDelegate) {
|
||
return oldDelegate.active != active;
|
||
}
|
||
}
|
||
|
||
/// The 14 px windowshade titlebar strip (`draw_pe_tbar` shade path).
|
||
class _PlShadeTitlebarPainter extends CustomPainter {
|
||
const _PlShadeTitlebarPainter({required this.image, required this.active});
|
||
|
||
final ui.Image image;
|
||
final bool active;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final paint = Paint()..filterQuality = FilterQuality.none;
|
||
final state = active ? 57.0 : 42.0;
|
||
final w = size.width;
|
||
var xp = 0.0;
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(72, 42, 25, 14),
|
||
Rect.fromLTWH(xp, 0, 25, 14),
|
||
paint,
|
||
);
|
||
xp += 25;
|
||
var n = ((w - 50) / 25).floor() - 1;
|
||
while (n > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(72, 57, 25, 14),
|
||
Rect.fromLTWH(xp, 0, 25, 14),
|
||
paint,
|
||
);
|
||
xp += 25;
|
||
n--;
|
||
}
|
||
final rem = (w - 50) % 25;
|
||
if (rem > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(72, 57, 24, 14),
|
||
Rect.fromLTWH(xp, 0, rem, 14),
|
||
paint,
|
||
);
|
||
xp += rem;
|
||
}
|
||
canvas.drawImageRect(
|
||
image,
|
||
Rect.fromLTWH(99, state, 50, 14),
|
||
Rect.fromLTWH(xp, 0, 50, 14),
|
||
paint,
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_PlShadeTitlebarPainter oldDelegate) {
|
||
return oldDelegate.active != active;
|
||
}
|
||
}
|
||
|
||
/// The left/right 29 px body border tiles (`draw_pl`).
|
||
class _PlBodyPainter extends CustomPainter {
|
||
const _PlBodyPainter(this.image);
|
||
|
||
final ui.Image image;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final paint = Paint()..filterQuality = FilterQuality.none;
|
||
final w = size.width;
|
||
var yp = 0.0;
|
||
var count = (size.height / 29).floor();
|
||
while (count > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(0, 42, 12, 29),
|
||
Rect.fromLTWH(0, yp, 12, 29),
|
||
paint,
|
||
);
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(31, 42, 5, 29),
|
||
Rect.fromLTWH(w - 20, yp, 5, 29),
|
||
paint,
|
||
);
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(44, 42, 7, 29),
|
||
Rect.fromLTWH(w - 7, yp, 7, 29),
|
||
paint,
|
||
);
|
||
yp += 29;
|
||
count--;
|
||
}
|
||
final rem = size.height % 29;
|
||
if (rem > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(0, 42, 12, 29),
|
||
Rect.fromLTWH(0, yp, 12, rem),
|
||
paint,
|
||
);
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(31, 42, 5, 29),
|
||
Rect.fromLTWH(w - 20, yp, 5, rem),
|
||
paint,
|
||
);
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(44, 42, 7, 29),
|
||
Rect.fromLTWH(w - 7, yp, 7, rem),
|
||
paint,
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_PlBodyPainter oldDelegate) {
|
||
return oldDelegate.image != image;
|
||
}
|
||
}
|
||
|
||
/// The 38 px bottom bar (`draw_pl`), including the five button-column backs.
|
||
class _PlBottomPainter extends CustomPainter {
|
||
const _PlBottomPainter(this.image);
|
||
|
||
final ui.Image image;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final paint = Paint()..filterQuality = FilterQuality.none;
|
||
final w = size.width;
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(0, 72, 125, 38),
|
||
const Rect.fromLTWH(0, 0, 125, 38),
|
||
paint,
|
||
);
|
||
var xp = 125.0;
|
||
final stretch = w - 125 - 150;
|
||
var count = (stretch / 25).floor();
|
||
var wide = false;
|
||
if (count >= 3) {
|
||
count -= 3;
|
||
wide = true;
|
||
}
|
||
while (count > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(179, 0, 25, 38),
|
||
Rect.fromLTWH(xp, 0, 25, 38),
|
||
paint,
|
||
);
|
||
xp += 25;
|
||
count--;
|
||
}
|
||
final rem = stretch % 25;
|
||
if (rem > 0) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(179, 0, 25, 38),
|
||
Rect.fromLTWH(xp, 0, rem, 38),
|
||
paint,
|
||
);
|
||
xp += rem;
|
||
}
|
||
if (wide) {
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(205, 0, 75, 38),
|
||
Rect.fromLTWH(xp, 0, 75, 38),
|
||
paint,
|
||
);
|
||
xp += 75;
|
||
}
|
||
canvas.drawImageRect(
|
||
image,
|
||
const Rect.fromLTWH(126, 72, 150, 38),
|
||
Rect.fromLTWH(xp, 0, 150, 38),
|
||
paint,
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_PlBottomPainter oldDelegate) {
|
||
return oldDelegate.image != image;
|
||
}
|
||
}
|
||
|
||
/// The song rows and vertical scrollbar (`draw_pl2` + `draw_pe_vslide`).
|
||
class _PlSongListPainter extends CustomPainter {
|
||
_PlSongListPainter({
|
||
required this.skin,
|
||
required this.keyed,
|
||
required this.offset,
|
||
required this.selected,
|
||
required this.dragging,
|
||
});
|
||
|
||
final ClassicSkin skin;
|
||
final _KeyedFont keyed;
|
||
final int offset;
|
||
final int selected;
|
||
final bool dragging;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
_paintRows(canvas);
|
||
_paintScrollbar(canvas);
|
||
}
|
||
|
||
void _paintRows(Canvas canvas) {
|
||
final colors = skin.pledit;
|
||
canvas.drawRect(
|
||
const Rect.fromLTWH(_listLeft, 0, _listRight - _listLeft, 3),
|
||
Paint()..color = colors.normalBg,
|
||
);
|
||
const rowsBottom = _rowsLocalTop + _numSongs * _rowStride;
|
||
canvas.drawRect(
|
||
const Rect.fromLTWH(
|
||
_listLeft,
|
||
rowsBottom,
|
||
_listRight - _listLeft,
|
||
232 - rowsBottom,
|
||
),
|
||
Paint()..color = colors.normalBg,
|
||
);
|
||
final visible = (_playlist.length - offset).clamp(0, _numSongs);
|
||
for (var i = 0; i < visible; i++) {
|
||
final absIndex = offset + i;
|
||
final y = _rowsLocalTop + i * _rowStride;
|
||
final isCurrent = absIndex == 0;
|
||
final isSelected = absIndex == selected;
|
||
canvas.drawRect(
|
||
Rect.fromLTWH(_listLeft, y, _listRight - _listLeft, _rowStride),
|
||
Paint()..color = isSelected ? colors.selectedBg : colors.normalBg,
|
||
);
|
||
final color = isCurrent ? colors.current : colors.normal;
|
||
_drawRowText(canvas, _playlist[absIndex], y + _rowGlyphOffset, color);
|
||
}
|
||
}
|
||
|
||
void _drawRowText(Canvas canvas, _Track track, double y, Color color) {
|
||
final time = _fmtTime(track.seconds);
|
||
final timeLen = time.length;
|
||
final nameBudget = 49 - (timeLen + 1);
|
||
var name = track.name;
|
||
if (name.length > nameBudget) {
|
||
name = "${name.substring(0, nameBudget - 3)}...";
|
||
}
|
||
keyed.draw(
|
||
canvas,
|
||
name,
|
||
_listLeft,
|
||
y,
|
||
color,
|
||
clipWidth: nameBudget * TextFont.charWidth,
|
||
);
|
||
keyed.draw(
|
||
canvas,
|
||
" ",
|
||
_listLeft + name.length * TextFont.charWidth,
|
||
y,
|
||
color,
|
||
);
|
||
keyed.draw(
|
||
canvas,
|
||
time,
|
||
_plWidth - 24 - timeLen * TextFont.charWidth,
|
||
y,
|
||
color,
|
||
);
|
||
}
|
||
|
||
void _paintScrollbar(Canvas canvas) {
|
||
final pledit = skin.image("pledit.bmp")!;
|
||
const trackH = 232;
|
||
const slidH = 18;
|
||
final t = _playlist.length - _numSongs;
|
||
int yp;
|
||
if (t < 1) {
|
||
yp = 0;
|
||
} else {
|
||
final p = offset.clamp(0, t);
|
||
yp = (214 * p) ~/ t;
|
||
}
|
||
const x = _plWidth - 15;
|
||
final paint = Paint()..filterQuality = FilterQuality.none;
|
||
var y = 0;
|
||
for (; y < yp - 28; y += 29) {
|
||
canvas.drawImageRect(
|
||
pledit,
|
||
const Rect.fromLTWH(36, 42, 8, 29),
|
||
Rect.fromLTWH(x, y.toDouble(), 8, 29),
|
||
paint,
|
||
);
|
||
}
|
||
var oy = y + 29;
|
||
if (slidH + yp > oy) oy += 29;
|
||
if (y < yp) {
|
||
canvas.drawImageRect(
|
||
pledit,
|
||
Rect.fromLTWH(36, 42, 8, (yp - y).toDouble()),
|
||
Rect.fromLTWH(x, y.toDouble(), 8, (yp - y).toDouble()),
|
||
paint,
|
||
);
|
||
}
|
||
canvas.drawImageRect(
|
||
pledit,
|
||
Rect.fromLTWH(dragging ? 61.0 : 52.0, 53, 8, 18),
|
||
Rect.fromLTWH(x, yp.toDouble(), 8, 18),
|
||
paint,
|
||
);
|
||
y = yp + 18;
|
||
if (y < oy && y < trackH) {
|
||
final h = y < trackH - 29 ? oy - y : trackH - y;
|
||
canvas.drawImageRect(
|
||
pledit,
|
||
Rect.fromLTWH(36, (42 + 29 - (oy - y)).toDouble(), 8, h.toDouble()),
|
||
Rect.fromLTWH(x, y.toDouble(), 8, h.toDouble()),
|
||
paint,
|
||
);
|
||
}
|
||
y = oy;
|
||
for (; y < trackH; y += 29) {
|
||
final h = y < trackH - 29 ? 29.0 : (trackH - y).toDouble();
|
||
canvas.drawImageRect(
|
||
pledit,
|
||
const Rect.fromLTWH(36, 42, 8, 29),
|
||
Rect.fromLTWH(x, y.toDouble(), 8, h),
|
||
paint,
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_PlSongListPainter oldDelegate) {
|
||
return oldDelegate.offset != offset ||
|
||
oldDelegate.selected != selected ||
|
||
oldDelegate.dragging != dragging;
|
||
}
|
||
}
|
||
|
||
/// The bottom-left `seltime/totaltime` string (`draw_pe_infostr`).
|
||
class _PlInfoPainter extends CustomPainter {
|
||
const _PlInfoPainter({required this.keyed, required this.text});
|
||
|
||
final _KeyedFont keyed;
|
||
final String text;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
var s = text;
|
||
s = s.length > 18 ? s.substring(0, 18) : s.padRight(18, " ");
|
||
keyed.draw(canvas, s, 0, 0, _displayGreen);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_PlInfoPainter oldDelegate) {
|
||
return oldDelegate.text != text;
|
||
}
|
||
}
|
||
|
||
/// The bottom-right `M:SS` elapsed display (`draw_pe_timedisp`).
|
||
class _PlTimePainter extends CustomPainter {
|
||
const _PlTimePainter({
|
||
required this.keyed,
|
||
required this.minutes,
|
||
required this.seconds,
|
||
});
|
||
|
||
final _KeyedFont keyed;
|
||
final int minutes;
|
||
final int seconds;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final m = minutes.clamp(0, 599);
|
||
final s = seconds.clamp(0, 59);
|
||
if (m < 100) {
|
||
keyed.draw(canvas, " ", 0, 0, _displayGreen, clipWidth: 4);
|
||
keyed.draw(canvas, " ", 4, 0, _displayGreen, clipWidth: 3);
|
||
keyed.draw(canvas, "${(m ~/ 10) % 10}", 9, 0, _displayGreen);
|
||
} else {
|
||
keyed.draw(canvas, " ", 0, 0, _displayGreen, clipWidth: 3);
|
||
keyed.draw(canvas, "${(m ~/ 100) % 10}", 4, 0, _displayGreen);
|
||
keyed.draw(canvas, "${(m ~/ 10) % 10}", 9, 0, _displayGreen);
|
||
}
|
||
keyed.draw(canvas, "${m % 10}", 14, 0, _displayGreen);
|
||
keyed.draw(canvas, "${(s ~/ 10) % 10}", 22, 0, _displayGreen);
|
||
keyed.draw(canvas, "${s % 10}", 27, 0, _displayGreen);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_PlTimePainter oldDelegate) {
|
||
return oldDelegate.minutes != minutes || oldDelegate.seconds != seconds;
|
||
}
|
||
}
|
||
|
||
/// The 5 button columns (add/rem/sel/misc/load) over the bottom bar.
|
||
class _PlButtons extends StatelessWidget {
|
||
const _PlButtons({required this.pledit});
|
||
|
||
final ui.Image pledit;
|
||
|
||
static const List<(double, double, Rect)> _bars = [
|
||
(11, 224, Rect.fromLTWH(48, 111, 3, 54)),
|
||
(40, 206, Rect.fromLTWH(100, 111, 3, 72)),
|
||
(69, 224, Rect.fromLTWH(150, 111, 3, 54)),
|
||
(98, 224, Rect.fromLTWH(200, 111, 3, 54)),
|
||
(_plWidth - 47, 224, Rect.fromLTWH(250, 111, 3, 54)),
|
||
];
|
||
|
||
static const List<(double, double, Rect)> _buttons = [
|
||
(14, 224, Rect.fromLTWH(0, 111, 22, 18)),
|
||
(14, 242, Rect.fromLTWH(0, 130, 22, 18)),
|
||
(14, 260, Rect.fromLTWH(0, 149, 22, 18)),
|
||
(43, 206, Rect.fromLTWH(54, 168, 22, 18)),
|
||
(43, 224, Rect.fromLTWH(54, 111, 22, 18)),
|
||
(43, 242, Rect.fromLTWH(54, 130, 22, 18)),
|
||
(43, 260, Rect.fromLTWH(54, 149, 22, 18)),
|
||
(72, 224, Rect.fromLTWH(104, 111, 22, 18)),
|
||
(72, 242, Rect.fromLTWH(104, 130, 22, 18)),
|
||
(72, 260, Rect.fromLTWH(104, 149, 22, 18)),
|
||
(101, 224, Rect.fromLTWH(154, 111, 22, 18)),
|
||
(101, 242, Rect.fromLTWH(154, 130, 22, 18)),
|
||
(101, 260, Rect.fromLTWH(154, 149, 22, 18)),
|
||
(_plWidth - 44, 224, Rect.fromLTWH(204, 111, 22, 18)),
|
||
(_plWidth - 44, 242, Rect.fromLTWH(204, 130, 22, 18)),
|
||
(_plWidth - 44, 260, Rect.fromLTWH(204, 149, 22, 18)),
|
||
];
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Stack(
|
||
children: [
|
||
for (final b in _bars)
|
||
Positioned(
|
||
left: b.$1,
|
||
top: b.$2,
|
||
child: SkinImage(image: pledit, src: b.$3),
|
||
),
|
||
for (final b in _buttons)
|
||
Positioned(
|
||
left: b.$1,
|
||
top: b.$2,
|
||
child: SkinImage(image: pledit, src: b.$3),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// The titlebar shade/close buttons (`draw_pe_tbutton`).
|
||
class _PlTitleButtons extends StatelessWidget {
|
||
const _PlTitleButtons({required this.pledit, required this.player});
|
||
|
||
final ui.Image pledit;
|
||
final PlayerState player;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Stack(
|
||
children: [
|
||
Positioned(
|
||
left: _plWidth - 20,
|
||
top: 3,
|
||
child: SkinPressButton(
|
||
image: pledit,
|
||
src: const Rect.fromLTWH(158, 3, 9, 9),
|
||
srcPressed: const Rect.fromLTWH(62, 42, 9, 9),
|
||
onTap: player.togglePlShade,
|
||
),
|
||
),
|
||
Positioned(
|
||
left: _plWidth - 11,
|
||
top: 3,
|
||
child: SkinPressButton(
|
||
image: pledit,
|
||
src: const Rect.fromLTWH(167, 3, 9, 9),
|
||
srcPressed: const Rect.fromLTWH(52, 42, 9, 9),
|
||
onTap: () => unawaited(_close()),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
static Future<void> _close() async {
|
||
await subWindowChannel.invokeMethod("plClose");
|
||
}
|
||
}
|
||
|
||
/// Windowshade (280×14) built from `pledit.bmp`.
|
||
class _PlShadeLayout extends StatelessWidget {
|
||
const _PlShadeLayout({required this.skin, required this.player});
|
||
|
||
final ClassicSkin skin;
|
||
final PlayerState player;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final pledit = skin.image("pledit.bmp")!;
|
||
final keyed = _KeyedFont(TextFont(skin.image("text.bmp")!));
|
||
return Stack(
|
||
children: [
|
||
CustomPaint(
|
||
size: const Size(_plWidth, _plShadeHeight),
|
||
painter: _PlShadeTitlebarPainter(image: pledit, active: true),
|
||
),
|
||
CustomPaint(
|
||
size: const Size(_plWidth, _plShadeHeight),
|
||
painter: _PlShadeSongPainter(keyed: keyed, track: _playlist[0]),
|
||
),
|
||
Positioned(
|
||
left: _plWidth - 20,
|
||
top: 3,
|
||
child: SkinPressButton(
|
||
image: pledit,
|
||
src: const Rect.fromLTWH(128, 45, 9, 9),
|
||
srcPressed: const Rect.fromLTWH(150, 42, 9, 9),
|
||
onTap: player.togglePlShade,
|
||
),
|
||
),
|
||
Positioned(
|
||
left: _plWidth - 11,
|
||
top: 3,
|
||
child: SkinPressButton(
|
||
image: pledit,
|
||
src: const Rect.fromLTWH(167, 3, 9, 9),
|
||
srcPressed: const Rect.fromLTWH(52, 42, 9, 9),
|
||
onTap: () => unawaited(_close()),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
static Future<void> _close() async {
|
||
await subWindowChannel.invokeMethod("plClose");
|
||
}
|
||
}
|
||
|
||
/// The windowshade current-song line (`draw_pe_song` with `pos = -1`).
|
||
class _PlShadeSongPainter extends CustomPainter {
|
||
const _PlShadeSongPainter({required this.keyed, required this.track});
|
||
|
||
final _KeyedFont keyed;
|
||
final _Track track;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final time = _fmtTime(track.seconds);
|
||
final timeLen = time.length;
|
||
final budget = 49 - (timeLen + 1);
|
||
var name = track.name;
|
||
if (name.length > budget) {
|
||
name = "${name.substring(0, budget - 3)}...";
|
||
}
|
||
const y = 4.0;
|
||
keyed.draw(
|
||
canvas,
|
||
name,
|
||
4,
|
||
y,
|
||
_displayGreen,
|
||
clipWidth: budget * TextFont.charWidth,
|
||
);
|
||
keyed.draw(
|
||
canvas,
|
||
" ",
|
||
4 + name.length * TextFont.charWidth,
|
||
y,
|
||
_displayGreen,
|
||
);
|
||
keyed.draw(
|
||
canvas,
|
||
time,
|
||
_plWidth - 29 - timeLen * TextFont.charWidth,
|
||
y,
|
||
_displayGreen,
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_PlShadeSongPainter oldDelegate) {
|
||
return oldDelegate.track != track;
|
||
}
|
||
}
|