Files
tasq/lib/screens/network_map/widgets/zoom_controls.dart
T

131 lines
3.9 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.
import 'package:flutter/material.dart';
/// Vertical +//fit-to-view zoom controls for the topology canvas.
/// Requires a [TransformationController] shared with the [InteractiveViewer].
class ZoomControls extends StatelessWidget {
const ZoomControls({
super.key,
required this.controller,
required this.canvasSize,
required this.viewportSize,
});
final TransformationController controller;
/// Full canvas size (from [computeSugiyamaLayout] result).
final Size canvasSize;
/// Viewport widget size (the screen area the canvas is displayed in).
final Size viewportSize;
static const double _zoomIn = 1.25;
static const double _zoomOut = 0.8;
static const double _minScale = 0.2;
static const double _maxScale = 2.5;
void _applyZoom(double factor) {
final current = controller.value.getMaxScaleOnAxis();
final next = (current * factor).clamp(_minScale, _maxScale);
final scale = next / current;
final cx = viewportSize.width / 2;
final cy = viewportSize.height / 2;
// Compose: T(cx,cy) * Scale(s) * T(-cx,-cy) * currentTransform
final result = Matrix4.translationValues(cx, cy, 0);
result.multiply(Matrix4.diagonal3Values(scale, scale, 1.0));
result.multiply(Matrix4.translationValues(-cx, -cy, 0));
result.multiply(controller.value);
controller.value = result;
}
void _fitToView() {
if (canvasSize.isEmpty) return;
final scaleX = viewportSize.width / canvasSize.width;
final scaleY = viewportSize.height / canvasSize.height;
final scale = (scaleX < scaleY ? scaleX : scaleY).clamp(_minScale, _maxScale);
final tx = (viewportSize.width - canvasSize.width * scale) / 2;
final ty = (viewportSize.height - canvasSize.height * scale) / 2;
// Build: T(tx,ty,0) * Scale(s,s,1) — maps canvas (0,0) → (tx,ty) on screen
final result = Matrix4.translationValues(tx, ty, 0);
result.multiply(Matrix4.diagonal3Values(scale, scale, 1.0));
controller.value = result;
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
elevation: 2,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_ZoomButton(
icon: Icons.add,
tooltip: 'Zoom in',
onTap: () => _applyZoom(_zoomIn),
),
_Divider(color: cs.outlineVariant.withValues(alpha: 0.4)),
_ZoomButton(
icon: Icons.remove,
tooltip: 'Zoom out',
onTap: () => _applyZoom(_zoomOut),
),
_Divider(color: cs.outlineVariant.withValues(alpha: 0.4)),
_ZoomButton(
icon: Icons.fit_screen_outlined,
tooltip: 'Fit to view',
onTap: _fitToView,
),
],
),
),
);
}
}
class _ZoomButton extends StatelessWidget {
const _ZoomButton({
required this.icon,
required this.tooltip,
required this.onTap,
});
final IconData icon;
final String tooltip;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Tooltip(
message: tooltip,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 40,
height: 40,
child: Icon(icon, size: 18, color: cs.onSurfaceVariant),
),
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider({required this.color});
final Color color;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Divider(height: 1, color: color),
);
}
}