From 080a6edfa59840d744f64f958062dbc725a687c7 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 18 Sep 2024 12:29:09 +0200 Subject: [PATCH] Add zoom --- app/lib/bloc/world/bloc.dart | 9 +++ app/lib/bloc/world/local.dart | 8 ++ app/lib/bloc/world/local.mapper.dart | 112 +++++++++++++++++++++++++++ app/lib/bloc/world/state.dart | 2 + app/lib/bloc/world/state.mapper.dart | 19 +++-- app/lib/board/cell.dart | 2 +- app/lib/board/grid.dart | 57 ++++++++++---- app/lib/helpers/vector.dart | 2 +- app/lib/l10n/app_en.arb | 6 +- app/lib/pages/game/drawer.dart | 36 +++++++++ app/pubspec.lock | 4 +- app/pubspec.yaml | 2 +- 12 files changed, 233 insertions(+), 26 deletions(-) diff --git a/app/lib/bloc/world/bloc.dart b/app/lib/bloc/world/bloc.dart index 2c2cd41..1a22031 100644 --- a/app/lib/bloc/world/bloc.dart +++ b/app/lib/bloc/world/bloc.dart @@ -89,6 +89,15 @@ class WorldBloc extends Bloc { on((event, emit) { emit(state.copyWith(drawerView: event.view)); }); + on((event, emit) { + var zoom = event.zoom; + if (zoom != null) { + zoom += state.zoom; + zoom = zoom.clamp(0.5, 2.0); + } + zoom ??= 1; + emit(state.copyWith(zoom: zoom)); + }); } Future save() async { diff --git a/app/lib/bloc/world/local.dart b/app/lib/bloc/world/local.dart index ff4e4b5..2002757 100644 --- a/app/lib/bloc/world/local.dart +++ b/app/lib/bloc/world/local.dart @@ -58,3 +58,11 @@ final class DrawerViewChanged extends LocalWorldEvent DrawerViewChanged(this.view); } + +@MappableClass() +final class ZoomChanged extends LocalWorldEvent with ZoomChangedMappable { + final double? zoom; + + ZoomChanged(this.zoom); + ZoomChanged.reset() : zoom = null; +} diff --git a/app/lib/bloc/world/local.mapper.dart b/app/lib/bloc/world/local.mapper.dart index 53ee334..4403aef 100644 --- a/app/lib/bloc/world/local.mapper.dart +++ b/app/lib/bloc/world/local.mapper.dart @@ -727,3 +727,115 @@ class _DrawerViewChangedCopyWithImpl<$R, $Out> Then<$Out2, $R2> t) => _DrawerViewChangedCopyWithImpl($value, $cast, t); } + +class ZoomChangedMapper extends SubClassMapperBase { + ZoomChangedMapper._(); + + static ZoomChangedMapper? _instance; + static ZoomChangedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = ZoomChangedMapper._()); + LocalWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + } + return _instance!; + } + + @override + final String id = 'ZoomChanged'; + + static double? _$zoom(ZoomChanged v) => v.zoom; + static const Field _f$zoom = Field('zoom', _$zoom); + + @override + final MappableFields fields = const { + #zoom: _f$zoom, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'ZoomChanged'; + @override + late final ClassMapperBase superMapper = + LocalWorldEventMapper.ensureInitialized(); + + static ZoomChanged _instantiate(DecodingData data) { + return ZoomChanged(data.dec(_f$zoom)); + } + + @override + final Function instantiate = _instantiate; + + static ZoomChanged fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static ZoomChanged fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin ZoomChangedMappable { + String toJson() { + return ZoomChangedMapper.ensureInitialized() + .encodeJson(this as ZoomChanged); + } + + Map toMap() { + return ZoomChangedMapper.ensureInitialized() + .encodeMap(this as ZoomChanged); + } + + ZoomChangedCopyWith get copyWith => + _ZoomChangedCopyWithImpl(this as ZoomChanged, $identity, $identity); + @override + String toString() { + return ZoomChangedMapper.ensureInitialized() + .stringifyValue(this as ZoomChanged); + } + + @override + bool operator ==(Object other) { + return ZoomChangedMapper.ensureInitialized() + .equalsValue(this as ZoomChanged, other); + } + + @override + int get hashCode { + return ZoomChangedMapper.ensureInitialized().hashValue(this as ZoomChanged); + } +} + +extension ZoomChangedValueCopy<$R, $Out> + on ObjectCopyWith<$R, ZoomChanged, $Out> { + ZoomChangedCopyWith<$R, ZoomChanged, $Out> get $asZoomChanged => + $base.as((v, t, t2) => _ZoomChangedCopyWithImpl(v, t, t2)); +} + +abstract class ZoomChangedCopyWith<$R, $In extends ZoomChanged, $Out> + implements LocalWorldEventCopyWith<$R, $In, $Out> { + @override + $R call({double? zoom}); + ZoomChangedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _ZoomChangedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, ZoomChanged, $Out> + implements ZoomChangedCopyWith<$R, ZoomChanged, $Out> { + _ZoomChangedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + ZoomChangedMapper.ensureInitialized(); + @override + $R call({Object? zoom = $none}) => + $apply(FieldCopyWithData({if (zoom != $none) #zoom: zoom})); + @override + ZoomChanged $make(CopyWithData data) => + ZoomChanged(data.get(#zoom, or: $value.zoom)); + + @override + ZoomChangedCopyWith<$R2, ZoomChanged, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _ZoomChangedCopyWithImpl($value, $cast, t); +} diff --git a/app/lib/bloc/world/state.dart b/app/lib/bloc/world/state.dart index 52e5b6c..131d040 100644 --- a/app/lib/bloc/world/state.dart +++ b/app/lib/bloc/world/state.dart @@ -29,6 +29,7 @@ final class ClientWorldState extends WorldState with ClientWorldStateMappable { final ItemLocation? selectedDeck; final bool showHand, switchCellOnMove; final DrawerView drawerView; + final double zoom; const ClientWorldState({ required this.multiplayer, @@ -48,6 +49,7 @@ final class ClientWorldState extends WorldState with ClientWorldStateMappable { super.messages, required super.data, this.drawerView = DrawerView.chat, + this.zoom = 1.0, }); QuokkaFileSystem get fileSystem => assetManager.fileSystem; diff --git a/app/lib/bloc/world/state.mapper.dart b/app/lib/bloc/world/state.mapper.dart index e2754fb..da68907 100644 --- a/app/lib/bloc/world/state.mapper.dart +++ b/app/lib/bloc/world/state.mapper.dart @@ -172,6 +172,9 @@ class ClientWorldStateMapper extends ClassMapperBase { static DrawerView _$drawerView(ClientWorldState v) => v.drawerView; static const Field _f$drawerView = Field('drawerView', _$drawerView, opt: true, def: DrawerView.chat); + static double _$zoom(ClientWorldState v) => v.zoom; + static const Field _f$zoom = + Field('zoom', _$zoom, opt: true, def: 1.0); @override final MappableFields fields = const { @@ -192,6 +195,7 @@ class ClientWorldStateMapper extends ClassMapperBase { #messages: _f$messages, #data: _f$data, #drawerView: _f$drawerView, + #zoom: _f$zoom, }; static ClientWorldState _instantiate(DecodingData data) { @@ -212,7 +216,8 @@ class ClientWorldStateMapper extends ClassMapperBase { teamMembers: data.dec(_f$teamMembers), messages: data.dec(_f$messages), data: data.dec(_f$data), - drawerView: data.dec(_f$drawerView)); + drawerView: data.dec(_f$drawerView), + zoom: data.dec(_f$zoom)); } @override @@ -302,7 +307,8 @@ abstract class ClientWorldStateCopyWith<$R, $In extends ClientWorldState, $Out> Map>? teamMembers, List? messages, QuokkaData? data, - DrawerView? drawerView}); + DrawerView? drawerView, + double? zoom}); ClientWorldStateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t); } @@ -360,7 +366,8 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> Map>? teamMembers, List? messages, QuokkaData? data, - DrawerView? drawerView}) => + DrawerView? drawerView, + double? zoom}) => $apply(FieldCopyWithData({ if (multiplayer != null) #multiplayer: multiplayer, if (colorScheme != null) #colorScheme: colorScheme, @@ -378,7 +385,8 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> if (teamMembers != null) #teamMembers: teamMembers, if (messages != null) #messages: messages, if (data != null) #data: data, - if (drawerView != null) #drawerView: drawerView + if (drawerView != null) #drawerView: drawerView, + if (zoom != null) #zoom: zoom })); @override ClientWorldState $make(CopyWithData data) => ClientWorldState( @@ -399,7 +407,8 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> teamMembers: data.get(#teamMembers, or: $value.teamMembers), messages: data.get(#messages, or: $value.messages), data: data.get(#data, or: $value.data), - drawerView: data.get(#drawerView, or: $value.drawerView)); + drawerView: data.get(#drawerView, or: $value.drawerView), + zoom: data.get(#zoom, or: $value.zoom)); @override ClientWorldStateCopyWith<$R2, ClientWorldState, $Out2> $chain<$R2, $Out2>( diff --git a/app/lib/board/cell.dart b/app/lib/board/cell.dart index b7a319a..a40d811 100644 --- a/app/lib/board/cell.dart +++ b/app/lib/board/cell.dart @@ -135,7 +135,7 @@ class GameCell extends PositionComponent } VectorDefinition toDefinition() => - (position.clone()..divide(grid.cellSize)).toDefinition(); + (position.clone()..divide(grid.cellSizeWithZoom)).toDefinition(); GlobalVectorDefinition toGlobalDefinition(ClientWorldState state) => GlobalVectorDefinition.fromLocal(state.tableName, toDefinition()); diff --git a/app/lib/board/grid.dart b/app/lib/board/grid.dart index 0708465..09be840 100644 --- a/app/lib/board/grid.dart +++ b/app/lib/board/grid.dart @@ -1,11 +1,16 @@ import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:quokka/bloc/world/bloc.dart'; +import 'package:quokka/bloc/world/state.dart'; import 'package:quokka/board/cell.dart'; -class BoardGrid extends PositionComponent with HasGameRef { +class BoardGrid extends PositionComponent + with HasGameRef, FlameBlocListenable { final Vector2 cellSize; static const _padding = 3.0; Rect? _lastViewport; + double _zoom = 1.0; BoardGrid({ required this.cellSize, @@ -13,11 +18,12 @@ class BoardGrid extends PositionComponent with HasGameRef { Rect get viewport { final Rect viewport = game.camera.visibleWorldRect; + final currentSize = cellSizeWithZoom; return Rect.fromLTRB( - (viewport.left / cellSize.x - _padding).floor() * cellSize.x, - (viewport.top / cellSize.y - _padding).floor() * cellSize.y, - (viewport.right / cellSize.x + _padding).ceil() * cellSize.x, - (viewport.bottom / cellSize.y + _padding).ceil() * cellSize.y, + (viewport.left / currentSize.x - _padding).floor() * currentSize.x, + (viewport.top / currentSize.y - _padding).floor() * currentSize.y, + (viewport.right / currentSize.x + _padding).ceil() * currentSize.x, + (viewport.bottom / currentSize.y + _padding).ceil() * currentSize.y, ); } @@ -32,6 +38,7 @@ class BoardGrid extends PositionComponent with HasGameRef { void _updateGrid() { if (!shouldReset()) return; final viewport = this.viewport; + final currentSize = cellSizeWithZoom; // Remove components that are out of the viewport removeAll(children.where((element) { if (element is! PositionComponent) return false; @@ -41,38 +48,40 @@ class BoardGrid extends PositionComponent with HasGameRef { final last = _lastViewport ?? Rect.zero; // Add components that are in the viewport // Top and bottom - for (var x = viewport.left; x < viewport.right; x += cellSize.x) { - for (var y = viewport.top; y < last.top; y += cellSize.y) { + for (var x = viewport.left; x < viewport.right; x += currentSize.x) { + for (var y = viewport.top; y < last.top; y += currentSize.y) { add(_createCell( position: Vector2(x, y), - size: cellSize, + size: currentSize, )); } - for (var y = last.bottom; y < viewport.bottom; y += cellSize.y) { + for (var y = last.bottom; y < viewport.bottom; y += currentSize.y) { add(_createCell( position: Vector2(x, y), - size: cellSize, + size: currentSize, )); } } // Left and right - for (var y = last.top; y < last.bottom; y += cellSize.y) { - for (var x = viewport.left; x < last.left; x += cellSize.x) { + for (var y = last.top; y < last.bottom; y += currentSize.y) { + for (var x = viewport.left; x < last.left; x += currentSize.x) { add(_createCell( position: Vector2(x, y), - size: cellSize, + size: currentSize, )); } - for (var x = last.right; x < viewport.right; x += cellSize.x) { + for (var x = last.right; x < viewport.right; x += currentSize.x) { add(_createCell( position: Vector2(x, y), - size: cellSize, + size: currentSize, )); } } _lastViewport = viewport; } + Vector2 get cellSizeWithZoom => cellSize * _zoom; + @override void update(double dt) { super.update(dt); @@ -90,4 +99,22 @@ class BoardGrid extends PositionComponent with HasGameRef { position: position, size: size, ); + + @override + void onInitialState(ClientWorldState state) { + _zoom = state.zoom; + } + + @override + bool listenWhen(ClientWorldState previousState, ClientWorldState newState) => + previousState.zoom != newState.zoom; + + @override + void onNewState(ClientWorldState state) { + if (_zoom != state.zoom) { + _zoom = state.zoom; + _lastViewport = null; + removeAll(children); + } + } } diff --git a/app/lib/helpers/vector.dart b/app/lib/helpers/vector.dart index 933f57d..bf313ab 100644 --- a/app/lib/helpers/vector.dart +++ b/app/lib/helpers/vector.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:quokka_api/quokka_api.dart'; extension VectorToDefinition on Vector2 { - VectorDefinition toDefinition() => VectorDefinition(x.toInt(), y.toInt()); + VectorDefinition toDefinition() => VectorDefinition(x.round(), y.round()); } extension DefinitionToVector on VectorDefinition { diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 31eaaa3..7daee64 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -180,5 +180,9 @@ "highContrast": "High contrast", "noServers": "There are no servers available", "expand": "Expand", - "collapse": "Collapse" + "collapse": "Collapse", + "zoom": "Zoom", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "resetZoom": "Reset zoom" } diff --git a/app/lib/pages/game/drawer.dart b/app/lib/pages/game/drawer.dart index a851511..14ea2e9 100644 --- a/app/lib/pages/game/drawer.dart +++ b/app/lib/pages/game/drawer.dart @@ -121,6 +121,42 @@ class GameDrawer extends StatelessWidget { ); }, ), + BlocBuilder( + buildWhen: (previous, current) => previous.zoom != current.zoom, + builder: (context, state) => ListTile( + leading: const Icon(PhosphorIconsLight.magnifyingGlass), + title: Text( + AppLocalizations.of(context).zoom, + ), + subtitle: Text( + '${(state.zoom * 100).toStringAsFixed(0)}%', + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(PhosphorIconsLight.minus), + tooltip: AppLocalizations.of(context).zoomOut, + onPressed: () => + context.read().process(ZoomChanged(-0.1)), + ), + IconButton( + icon: const Icon(PhosphorIconsLight.plus), + tooltip: AppLocalizations.of(context).zoomIn, + onPressed: () => + context.read().process(ZoomChanged(0.1)), + ), + IconButton( + icon: const Icon(PhosphorIconsLight.clockClockwise), + tooltip: AppLocalizations.of(context).resetZoom, + onPressed: () { + context.read().process(ZoomChanged.reset()); + }, + ), + ], + ), + ), + ), ListTile( leading: const Icon(PhosphorIconsLight.package), title: Text(AppLocalizations.of(context).packs), diff --git a/app/pubspec.lock b/app/pubspec.lock index 38ebccc..15fd858 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -387,8 +387,8 @@ packages: dependency: "direct main" description: path: "." - ref: d1fb4e2225b039bc8cd0bbadb6f3e7b459ae2fe4 - resolved-ref: d1fb4e2225b039bc8cd0bbadb6f3e7b459ae2fe4 + ref: fe087eb6b5eb4eb4ed6026a12a515fecb9c8a891 + resolved-ref: fe087eb6b5eb4eb4ed6026a12a515fecb9c8a891 url: "https://github.com/CodeDoctorDE/flex_color_scheme" source: git version: "8.0.0-dev.1" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 2f8a8b0..256b887 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: flex_color_scheme: git: url: https://github.com/CodeDoctorDE/flex_color_scheme - ref: d1fb4e2225b039bc8cd0bbadb6f3e7b459ae2fe4 + ref: fe087eb6b5eb4eb4ed6026a12a515fecb9c8a891 flutter_svg: ^2.0.10+1 window_manager: ^0.4.0 go_router: ^14.2.2