diff --git a/lib/widgets/map_svg_color_mapper.dart b/lib/widgets/map_svg_color_mapper.dart index 50dda7e6..bf3f2405 100644 --- a/lib/widgets/map_svg_color_mapper.dart +++ b/lib/widgets/map_svg_color_mapper.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart' show mapEquals; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:icarus/providers/user_preferences_provider.dart'; @@ -12,24 +13,26 @@ class MapSvgSourceColors { } class MapSvgColorMapper extends ColorMapper { - const MapSvgColorMapper(this.replacements); + MapSvgColorMapper(Map replacements) + : replacements = Map.unmodifiable(replacements); MapSvgColorMapper.forPalette(MapThemePalette palette) - : replacements = { + : replacements = Map.unmodifiable({ MapSvgSourceColors.base.toARGB32(): palette.baseColor, MapSvgSourceColors.detail.toARGB32(): palette.detailColor, MapSvgSourceColors.highlight.toARGB32(): palette.highlightColor, - }; + }); /// The map in [palette] as cones reveal it: the same art with its floor /// in [revealedFloorColor] (see `lib/view_cone/cone_coverage.dart`). MapSvgColorMapper.revealed(MapThemePalette palette) - : replacements = { + : replacements = Map.unmodifiable({ MapSvgSourceColors.base.toARGB32(): revealedFloorColor(palette), MapSvgSourceColors.detail.toARGB32(): palette.detailColor, MapSvgSourceColors.highlight.toARGB32(): palette.highlightColor, - }; + }); + /// Unmodifiable: mappers are compared by these, so they never change. final Map replacements; @override @@ -48,4 +51,18 @@ class MapSvgColorMapper extends ColorMapper { final alpha = (color.a * 255.0).round().clamp(0, 255); return replacement.withAlpha(alpha); } + + // The SVG loader and its picture cache compare mappers, so a mapper made + // again with the same colours, as views do on every build (a replay builds + // every frame), must equal the last one. If it didn't, the map would be + // loaded and drawn again from scratch. + @override + bool operator ==(Object other) => + other is MapSvgColorMapper && mapEquals(other.replacements, replacements); + + @override + int get hashCode => Object.hashAllUnordered([ + for (final MapEntry(:key, :value) in replacements.entries) + Object.hash(key, value), + ]); } diff --git a/test/map_svg_color_mapper_test.dart b/test/map_svg_color_mapper_test.dart new file mode 100644 index 00000000..cd5684d7 --- /dev/null +++ b/test/map_svg_color_mapper_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter/painting.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:icarus/providers/user_preferences_provider.dart'; +import 'package:icarus/widgets/map_svg_color_mapper.dart'; + +void main() { + final palette = MapThemePalette( + baseColorValue: 0xFF271406, + detailColorValue: 0xFFB27C40, + highlightColorValue: 0xFFF08234, + ); + + // A map widget rebuilt with a mapper made again from the same palette must + // keep its loaded picture: flutter_svg reloads whenever the loader changes, + // and the loader compares mappers. + test('a mapper made again from the same palette gives an equal loader', () { + SvgAssetLoader loader(MapSvgColorMapper mapper) => + SvgAssetLoader('assets/maps/ascent_map.svg', colorMapper: mapper); + + for (final make in [ + MapSvgColorMapper.forPalette, + MapSvgColorMapper.revealed, + ]) { + final again = make(palette.copyWith()); + expect(again, make(palette)); + expect(again.hashCode, make(palette).hashCode); + expect(loader(again), loader(make(palette))); + } + }); + + test('the order colours were listed in does not matter', () { + const red = Color(0xFFFF0000), blue = Color(0xFF0000FF); + final one = MapSvgColorMapper(const {1: red, 2: blue}); + final other = MapSvgColorMapper(const {2: blue, 1: red}); + expect(other, one); + expect(other.hashCode, one.hashCode); + }); + + test('mappers for different colours differ', () { + final other = palette.copyWith(baseColorValue: 0xFF102030); + expect(MapSvgColorMapper.forPalette(other), + isNot(MapSvgColorMapper.forPalette(palette))); + expect(MapSvgColorMapper.revealed(palette), + isNot(MapSvgColorMapper.forPalette(palette))); + }); +}