diff --git a/Furious/Data/maplibre/EndpointMap.html b/Furious/Data/maplibre/EndpointMap.html index ac463a6..d5a3da6 100644 --- a/Furious/Data/maplibre/EndpointMap.html +++ b/Furious/Data/maplibre/EndpointMap.html @@ -14,23 +14,33 @@ height: 100%; margin: 0; overflow: hidden; - background: transparent; + background: var(--endpoint-surface); } :root { - --endpoint-attribution-background: rgb(255 255 255 / 92%); - --endpoint-attribution-foreground: #202020; - --endpoint-loading-background: rgb(255 255 255 / 32%); - --endpoint-loading-foreground: #202020; + --endpoint-surface: #f8fafd; + --endpoint-border: #d9e1ec; + --endpoint-text: #172033; + --endpoint-text-muted: #5e6b80; + --endpoint-marker-halo: rgb(23 32 51 / 24%); + --endpoint-attribution-background: rgb(248 250 253 / 92%); + --endpoint-loading-background: rgb(248 250 253 / 86%); --endpoint-font-family: sans-serif; --endpoint-font-size: 11pt; } :root[data-theme="dark"] { - --endpoint-attribution-background: rgb(24 30 39 / 92%); - --endpoint-attribution-foreground: #f5f5f5; - --endpoint-loading-background: rgb(16 21 28 / 38%); - --endpoint-loading-foreground: #f5f5f5; + --endpoint-surface: #11161d; + --endpoint-border: #2a3340; + --endpoint-text: #e7ecf4; + --endpoint-text-muted: #9aa7b8; + --endpoint-marker-halo: rgb(255 255 255 / 24%); + --endpoint-attribution-background: rgb(17 22 29 / 92%); + --endpoint-loading-background: rgb(17 22 29 / 86%); + } + + body, #map, #endpoint-loading-overlay { + border-radius: 6px; } #endpoint-loading-overlay { @@ -41,7 +51,7 @@ align-items: center; justify-content: center; gap: 8px; - color: var(--endpoint-loading-foreground); + color: var(--endpoint-text); background: var(--endpoint-loading-background); cursor: wait; font-family: var(--endpoint-font-family); @@ -71,13 +81,15 @@ } .endpoint-marker { - width: 20px; - height: 20px; + width: 18px; + height: 18px; box-sizing: border-box; - border: 3px solid white; + border: 2px solid white; border-radius: 50%; background: var(--endpoint-accent, #0f7bff); - box-shadow: 0 1px 5px rgb(0 0 0 / 45%); + box-shadow: + 0 0 0 2px var(--endpoint-marker-halo), + 0 2px 6px rgb(0 0 0 / 34%); } .maplibregl-canvas:focus, @@ -87,9 +99,13 @@ .maplibregl-ctrl-attrib.maplibregl-compact { max-width: calc(100% - 20px); - color: var(--endpoint-attribution-foreground) !important; + color: var(--endpoint-text-muted) !important; background-color: var(--endpoint-attribution-background) !important; - box-shadow: 0 1px 4px rgb(0 0 0 / 24%); + border: 1px solid var(--endpoint-border); + border-radius: 7px 0 0 0; + box-shadow: none; + font-family: var(--endpoint-font-family); + font-size: 10px; } .maplibregl-ctrl-attrib a { diff --git a/Furious/Data/maplibre/EndpointMap.js b/Furious/Data/maplibre/EndpointMap.js index 21be8ab..d752b09 100644 --- a/Furious/Data/maplibre/EndpointMap.js +++ b/Furious/Data/maplibre/EndpointMap.js @@ -6,7 +6,7 @@ let marker = null; let state = null; let appliedRevision = -1; - let activeDarkMode = null; + let activeStyleKey = ''; let initialReadyReported = false; const loadingOverlay = document.getElementById('endpoint-loading-overlay'); const loadingText = document.getElementById('endpoint-loading-text'); @@ -24,40 +24,46 @@ ], ]; - const createStyle = (darkMode) => { - const colors = darkMode - ? { - background: '#0d1117', - land: '#161c25', - landcover: '#18271f', - landuse: '#1b222c', - water: '#19334a', - boundary: '#536174', - road: '#3e4a59', - roadMajor: '#657489', - building: '#222c38', - label: '#eef4fb', - labelMuted: '#b9c5d3', - halo: '#111720', - waterLabel: '#8fc4e8', - poi: '#c6d0dc', - } - : { - background: '#edf1f5', - land: '#f7f8fa', - landcover: '#e2ebdf', - landuse: '#eceff3', - water: '#bad9ec', - boundary: '#a7b0bd', - road: '#ffffff', - roadMajor: '#d4aa72', - building: '#d9dce1', - label: '#202832', - labelMuted: '#536170', - halo: '#ffffff', - waterLabel: '#446f91', - poi: '#4e5b68', - }; + const MAP_PALETTES = { + dark: { + land: '#151a22', + landcover: '#19231f', + landuse: '#1a2028', + water: '#182734', + roadMinor: '#303a46', + roadMajor: '#465363', + building: '#202731', + waterLabel: '#8fa6b8', + }, + light: { + land: '#f7f9fc', + landcover: '#eaf0e9', + landuse: '#edf1f5', + water: '#e2ebf2', + roadMinor: '#dce4ed', + roadMajor: '#becad8', + building: '#e4e9ef', + waterLabel: '#607d94', + }, + }; + + const createPalette = (themeState) => { + const darkMode = Boolean(themeState.darkMode); + const mapPalette = MAP_PALETTES[darkMode ? 'dark' : 'light']; + + return { + ...mapPalette, + background: themeState.surfaceColor || (darkMode ? '#11161d' : '#f8fafd'), + boundary: themeState.borderColor || (darkMode ? '#2a3340' : '#d9e1ec'), + label: themeState.textColor || (darkMode ? '#e7ecf4' : '#172033'), + labelMuted: themeState.mutedTextColor || (darkMode ? '#9aa7b8' : '#5e6b80'), + halo: themeState.surfaceColor || (darkMode ? '#11161d' : '#f8fafd'), + poi: themeState.mutedTextColor || (darkMode ? '#9aa7b8' : '#5e6b80'), + }; + }; + + const createStyle = (themeState) => { + const colors = createPalette(themeState); return { version: 8, @@ -90,7 +96,7 @@ 'source-layer': 'landcover', paint: { 'fill-color': colors.landcover, - 'fill-opacity': 0.55, + 'fill-opacity': 0.3, }, }, { @@ -100,7 +106,7 @@ 'source-layer': 'landuse', paint: { 'fill-color': colors.landuse, - 'fill-opacity': 0.5, + 'fill-opacity': 0.32, }, }, { @@ -118,7 +124,7 @@ paint: { 'line-color': colors.boundary, 'line-width': 0.8, - 'line-opacity': 0.7, + 'line-opacity': 0.52, }, }, { @@ -128,9 +134,9 @@ 'source-layer': 'transportation', minzoom: 5, paint: { - 'line-color': colors.road, + 'line-color': colors.roadMinor, 'line-width': 0.8, - 'line-opacity': 0.65, + 'line-opacity': 0.46, }, }, { @@ -157,7 +163,7 @@ 14, 2.4, ], - 'line-opacity': 0.85, + 'line-opacity': 0.72, }, }, { @@ -165,10 +171,10 @@ type: 'fill', source: 'openmaptiles', 'source-layer': 'building', - minzoom: 13, + minzoom: 14, paint: { 'fill-color': colors.building, - 'fill-opacity': 0.72, + 'fill-opacity': 0.42, }, }, { @@ -196,6 +202,7 @@ 'text-color': colors.waterLabel, 'text-halo-color': colors.halo, 'text-halo-width': 1, + 'text-opacity': 0.76, }, }, { @@ -230,6 +237,7 @@ 'text-color': colors.labelMuted, 'text-halo-color': colors.halo, 'text-halo-width': 1, + 'text-opacity': 0.72, }, }, { @@ -237,7 +245,7 @@ type: 'symbol', source: 'openmaptiles', 'source-layer': 'transportation_name', - minzoom: 14, + minzoom: 15, filter: [ 'match', ['get', 'class'], @@ -256,6 +264,7 @@ 'text-color': colors.labelMuted, 'text-halo-color': colors.halo, 'text-halo-width': 1, + 'text-opacity': 0.58, }, }, { @@ -305,6 +314,7 @@ 'text-color': colors.labelMuted, 'text-halo-color': colors.halo, 'text-halo-width': 1, + 'text-opacity': 0.72, }, }, { @@ -347,7 +357,7 @@ type: 'symbol', source: 'openmaptiles', 'source-layer': 'poi', - minzoom: 15, + minzoom: 16, layout: { 'text-field': nameField, 'text-font': ['Noto Sans Regular'], @@ -358,6 +368,7 @@ 'text-color': colors.poi, 'text-halo-color': colors.halo, 'text-halo-width': 1, + 'text-opacity': 0.58, }, }, ], @@ -387,6 +398,40 @@ : ''; }; + const applyThemeState = () => { + if (!state) { + return; + } + + document.documentElement.dataset.theme = state.darkMode + ? 'dark' + : 'light'; + + const properties = { + '--endpoint-accent': state.accentColor, + '--endpoint-surface': state.surfaceColor, + '--endpoint-border': state.borderColor, + '--endpoint-text': state.textColor, + '--endpoint-text-muted': state.mutedTextColor, + '--endpoint-font-family': JSON.stringify(state.fontFamily || 'sans-serif'), + '--endpoint-font-size': `${state.fontPointSize || 11}pt`, + }; + + for (const [name, value] of Object.entries(properties)) { + if (value) { + document.documentElement.style.setProperty(name, value); + } + } + }; + + const styleKey = () => JSON.stringify([ + Boolean(state.darkMode), + state.surfaceColor, + state.borderColor, + state.textColor, + state.mutedTextColor, + ]); + const reportError = (event) => { const message = event && event.error && event.error.message ? event.error.message @@ -426,36 +471,20 @@ const applyState = () => { applyLoadingState(); + applyThemeState(); if (!state) { return; } - document.documentElement.dataset.theme = state.darkMode - ? 'dark' - : 'light'; - - document.documentElement.style.setProperty( - '--endpoint-accent', - state.accentColor - ); - document.documentElement.style.setProperty( - '--endpoint-font-family', - JSON.stringify(state.fontFamily || 'sans-serif') - ); - document.documentElement.style.setProperty( - '--endpoint-font-size', - `${state.fontPointSize || 11}pt` - ); - if (!map) { return; } - const nextDarkMode = Boolean(state.darkMode); - if (nextDarkMode !== activeDarkMode) { - activeDarkMode = nextDarkMode; - const nextStyle = createStyle(nextDarkMode); + const nextStyleKey = styleKey(); + if (nextStyleKey !== activeStyleKey) { + activeStyleKey = nextStyleKey; + const nextStyle = createStyle(state); map.setStyle(nextStyle); } @@ -478,10 +507,10 @@ return; } - activeDarkMode = Boolean(state.darkMode); + activeStyleKey = styleKey(); map = new maplibregl.Map({ container: 'map', - style: createStyle(activeDarkMode), + style: createStyle(state), center: initialCoordinate, zoom: state.defaultGeographicZoom, minZoom: 2, @@ -506,6 +535,7 @@ window.endpointMap = { setState(nextState) { state = nextState; + applyThemeState(); applyLoadingState(); createMap(); applyState(); diff --git a/Furious/Widget/EndpointInfoWidget.py b/Furious/Widget/EndpointInfoWidget.py index dc2d6a7..9667b0e 100644 --- a/Furious/Widget/EndpointInfoWidget.py +++ b/Furious/Widget/EndpointInfoWidget.py @@ -417,6 +417,7 @@ class _EndpointMapWidget(QWidget): return hasLocation = self._hasLocation() + themePalette = AppStyleSheet.paletteForTheme(self._theme) location, accentColor, font = ( self._location if hasLocation else None, @@ -432,6 +433,10 @@ class _EndpointMapWidget(QWidget): 'viewRevision': self._viewRevision, 'darkMode': self._theme == AppStyleSheet.Dark, 'accentColor': accentColor, + 'surfaceColor': themePalette['panel_alt'], + 'borderColor': themePalette['border'], + 'textColor': themePalette['text'], + 'mutedTextColor': themePalette['muted'], 'loading': self._loading or ( self._active diff --git a/tests/test_endpoint_info.py b/tests/test_endpoint_info.py index e1471f5..a6c3e96 100644 --- a/tests/test_endpoint_info.py +++ b/tests/test_endpoint_info.py @@ -54,6 +54,7 @@ from tests.support import ( import json from pathlib import Path +import re import unittest from unittest.mock import patch import weakref @@ -752,6 +753,23 @@ class EndpointInfoServiceTest(unittest.TestCase): widget.mapWidget._lastWebState['fontPointSize'], QtGui.QFontInfo(widget.mapWidget.font()).pointSizeF(), ) + themePalette = AppStyleSheet.paletteForTheme(widget.mapWidget._theme) + self.assertEqual( + widget.mapWidget._lastWebState['surfaceColor'], + themePalette['panel_alt'], + ) + self.assertEqual( + widget.mapWidget._lastWebState['borderColor'], + themePalette['border'], + ) + self.assertEqual( + widget.mapWidget._lastWebState['textColor'], + themePalette['text'], + ) + self.assertEqual( + widget.mapWidget._lastWebState['mutedTextColor'], + themePalette['muted'], + ) self.assertAlmostEqual(widget.mapWidget._lastWebState['markerLatitude'], 34.05) self.assertAlmostEqual( widget.mapWidget._lastWebState['markerLongitude'], -118.24 @@ -791,7 +809,7 @@ class EndpointInfoServiceTest(unittest.TestCase): self.assertIn('map.setStyle(nextStyle)', mapScript) self.assertIn("document.documentElement.dataset.theme", mapScript) self.assertIn('--endpoint-attribution-background', html) - self.assertIn('--endpoint-attribution-foreground', html) + self.assertIn('--endpoint-text-muted', html) self.assertIn('endpoint-loading-overlay', html) self.assertIn('endpoint-loading-spinner', html) self.assertIn('width: 16px', html) @@ -805,7 +823,11 @@ class EndpointInfoServiceTest(unittest.TestCase): self.assertIn('.maplibregl-canvas:focus', html) self.assertIn('outline: none', html) self.assertNotIn('color-mix(', html) - self.assertIn('const createStyle = (darkMode)', mapScript) + self.assertIn('const MAP_PALETTES = {', mapScript) + self.assertIn('const createPalette = (themeState)', mapScript) + self.assertIn('const createStyle = (themeState)', mapScript) + self.assertIn('style: createStyle(state)', mapScript) + self.assertIn('const styleKey = ()', mapScript) self.assertIn("url: 'https://tiles.openfreemap.org/planet'", mapScript) self.assertIn("glyphs: 'https://tiles.openfreemap.org/fonts/", mapScript) self.assertIn("'source-layer': 'water'", mapScript) @@ -891,6 +913,90 @@ class EndpointInfoServiceTest(unittest.TestCase): widget.deleteLater() service.deleteLater() + def testMapPaletteHasRestrainedThemeContrastAndLayerHierarchy(self): + """Keep the local basemap readable without overpowering Fluent cards.""" + mapScript = ( + Path(__file__).parents[1] + / 'Furious' + / 'Data' + / 'maplibre' + / 'EndpointMap.js' + ).read_text(encoding='utf-8') + + def mapPalette(theme): + match = re.search( + rf"{theme}: \{{(?P
.*?)\n \}}", + mapScript, + re.DOTALL, + ) + + self.assertIsNotNone(match) + + return dict( + re.findall( + r"(?P