summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2024-03-20 11:05:18 +0100
committerOskar Nyberg <oskar@mullvad.net>2024-03-20 11:05:18 +0100
commit4b4304f57a8236e349654f8841d09e6db1432dc2 (patch)
tree21757b043e3268e83066eb8ee1d8d9ff6d3c001d /gui/src
parent625a7874ec2341a268a67162b4d6af3e804d7473 (diff)
parent4ba956b8da8fb25f36b0ccac1c700b11205d7861 (diff)
downloadmullvadvpn-4b4304f57a8236e349654f8841d09e6db1432dc2.tar.xz
mullvadvpn-4b4304f57a8236e349654f8841d09e6db1432dc2.zip
Merge branch 'map-scaling-incorrectly-des-692'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/window-controller.ts4
-rw-r--r--gui/src/renderer/components/Map.tsx131
-rw-r--r--gui/src/renderer/lib/3dmap.ts4
-rw-r--r--gui/src/renderer/lib/utilityHooks.ts9
-rw-r--r--gui/src/shared/ipc-helpers.ts6
-rw-r--r--gui/src/shared/ipc-schema.ts1
6 files changed, 87 insertions, 68 deletions
diff --git a/gui/src/main/window-controller.ts b/gui/src/main/window-controller.ts
index 830f6fd1af..b18775a8b5 100644
--- a/gui/src/main/window-controller.ts
+++ b/gui/src/main/window-controller.ts
@@ -271,6 +271,10 @@ export default class WindowController {
_display: Display,
changedMetrics: string[],
) => {
+ if (changedMetrics.includes('scaleFactor')) {
+ IpcMainEventChannel.window.notifyScaleFactorChange?.();
+ }
+
if (changedMetrics.includes('workArea') && this.window?.isVisible()) {
this.onWorkAreaSizeChange();
if (process.platform === 'win32') {
diff --git a/gui/src/renderer/components/Map.tsx b/gui/src/renderer/components/Map.tsx
index b7758b2e7f..b002cce5e8 100644
--- a/gui/src/renderer/components/Map.tsx
+++ b/gui/src/renderer/components/Map.tsx
@@ -1,11 +1,11 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef } from 'react';
import styled from 'styled-components';
import { TunnelState } from '../../shared/daemon-rpc-types';
import log from '../../shared/logging';
import { useAppContext } from '../context';
import GlMap, { ConnectionState, Coordinate } from '../lib/3dmap';
-import { useCombinedRefs } from '../lib/utilityHooks';
+import { useCombinedRefs, useRerenderer } from '../lib/utilityHooks';
import { useSelector } from '../redux/store';
// Default to Gothenburg when we don't know the actual location.
@@ -22,8 +22,6 @@ interface MapParams {
connectionState: ConnectionState;
}
-type AnimationFrameCallback = (now: number, newParams?: MapParams) => void;
-
export default function Map() {
const connection = useSelector((state) => state.connection);
const animateMap = useSelector((state) => state.settings.guiSettings.animateMap);
@@ -77,29 +75,47 @@ interface MapInnerProps extends MapParams {
function MapInner(props: MapInnerProps) {
const { getMapData } = useAppContext();
- // Callback that should be passed to requestAnimationFrame. This is initialized after the canvas
- // has been rendered.
- const animationFrameCallback = useRef<AnimationFrameCallback>();
// When location or connection state changes it's stored here until passed to 3dmap
const newParams = useRef<MapParams>();
// This is set to true when rendering should be paused
const pause = useRef<boolean>(false);
+ const mapRef = useRef<GlMap>();
const canvasRef = useRef<HTMLCanvasElement>();
- const [canvasWidth, setCanvasWidth] = useState(window.innerWidth);
+ const width = applyPixelRatio(canvasRef.current?.clientWidth ?? window.innerWidth);
// This constant is used for the height the first frame that is rendered only.
- const [canvasHeight, setCanvasHeight] = useState(493);
+ const height = applyPixelRatio(canvasRef.current?.clientHeight ?? 493);
- const updateCanvasSize = useCallback((canvas: HTMLCanvasElement) => {
- const canvasRect = canvas.getBoundingClientRect();
+ // Hack to rerender when window size changes or when ref is set.
+ const [onSizeChange, sizeChangeCounter] = useRerenderer();
- canvas.width = applyScaleFactor(canvasRect.width);
- canvas.height = applyScaleFactor(canvasRect.height);
+ const render = useCallback(() => requestAnimationFrame(animationFrameCallback), []);
- setCanvasWidth(canvasRect.width);
- setCanvasHeight(canvasRect.height);
- }, []);
+ const animationFrameCallback = useCallback(
+ (now: number) => {
+ now *= 0.001; // convert to seconds
+
+ // Propagate location change to the map
+ if (newParams.current) {
+ mapRef.current?.setLocation(
+ newParams.current.location,
+ newParams.current.connectionState,
+ now,
+ props.animate,
+ );
+ newParams.current = undefined;
+ }
+
+ mapRef.current?.draw(now);
+
+ // Stops rendering if pause is true. This happens when there is no ongoing movements
+ if (!pause.current) {
+ render();
+ }
+ },
+ [props.animate],
+ );
// This is called when the canvas has been rendered the first time and initializes the gl context
// and the map.
@@ -108,11 +124,11 @@ function MapInner(props: MapInnerProps) {
return;
}
- updateCanvasSize(canvas);
+ onSizeChange();
const gl = canvas.getContext('webgl2', { antialias: true })!;
- const map = new GlMap(
+ mapRef.current = new GlMap(
gl,
await getMapData(),
props.location,
@@ -120,30 +136,7 @@ function MapInner(props: MapInnerProps) {
() => (pause.current = true),
);
- // Function to be used when calling requestAnimationFrame
- animationFrameCallback.current = (now: number) => {
- now *= 0.001; // convert to seconds
-
- // Propagate location change to the map
- if (newParams.current) {
- map.setLocation(
- newParams.current.location,
- newParams.current.connectionState,
- now,
- props.animate,
- );
- newParams.current = undefined;
- }
-
- map.draw(now);
-
- // Stops rendering if pause is true. This happens when there is no ongoing movements
- if (!pause.current) {
- requestAnimationFrame(animationFrameCallback.current!);
- }
- };
-
- requestAnimationFrame(animationFrameCallback.current);
+ render();
}, []);
// Set new params when the location or connection state has changed, and unpause if paused
@@ -155,41 +148,47 @@ function MapInner(props: MapInnerProps) {
if (pause.current) {
pause.current = false;
- if (animationFrameCallback.current) {
- requestAnimationFrame(animationFrameCallback.current);
- }
+ render();
}
}, [props.location, props.connectionState]);
+ useEffect(() => {
+ mapRef.current?.updateViewport();
+ render();
+ }, [width, height, sizeChangeCounter]);
+
// Resize canvas if window size changes
useEffect(() => {
- const resizeCallback = () => {
- if (canvasRef.current) {
- updateCanvasSize(canvasRef.current);
- }
- };
+ addEventListener('resize', onSizeChange);
+ return () => removeEventListener('resize', onSizeChange);
+ }, []);
- addEventListener('resize', resizeCallback);
- return () => removeEventListener('resize', resizeCallback);
- }, [updateCanvasSize]);
+ useEffect(() => {
+ const unsubscribe = window.ipc.window.listenScaleFactorChange(onSizeChange);
+ return () => unsubscribe();
+ }, []);
// Log new scale factor if it changes
- useEffect(() => log.verbose('Map canvas scale factor:', window.devicePixelRatio), [
- window.devicePixelRatio,
- ]);
+ useEffect(() => {
+ log.verbose(`Map canvas scale factor: ${window.devicePixelRatio}, using: ${getPixelRatio()}`);
+ }, [window.devicePixelRatio]);
const combinedCanvasRef = useCombinedRefs(canvasRef, canvasCallback);
- return (
- <StyledCanvas
- ref={combinedCanvasRef}
- width={applyScaleFactor(canvasWidth)}
- height={applyScaleFactor(canvasHeight)}
- />
- );
+ return <StyledCanvas ref={combinedCanvasRef} width={width} height={height} />;
+}
+
+function getPixelRatio(): number {
+ let pixelRatio = window.devicePixelRatio;
+
+ // Wayland renders non-integer values as the next integer and then scales it back down.
+ if (window.env.platform === 'linux') {
+ pixelRatio = Math.ceil(pixelRatio);
+ }
+
+ return pixelRatio;
}
-function applyScaleFactor(dimension: number): number {
- const scaleFactor = window.devicePixelRatio;
- return Math.floor(dimension * scaleFactor);
+function applyPixelRatio(dimension: number): number {
+ return Math.floor(dimension * getPixelRatio());
}
diff --git a/gui/src/renderer/lib/3dmap.ts b/gui/src/renderer/lib/3dmap.ts
index 61f86d97b9..f74efcf5df 100644
--- a/gui/src/renderer/lib/3dmap.ts
+++ b/gui/src/renderer/lib/3dmap.ts
@@ -451,6 +451,10 @@ export default class GlMap {
this.zoomAnimations = [];
}
+ public updateViewport() {
+ this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);
+ }
+
// Move the location marker to `newCoordinate` (with state `connectionState`).
// Queues an animation to `newCoordinate` if `animate` is true. Otherwise it moves
// directly to that location.
diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts
index 8c3925762e..81a5579dd6 100644
--- a/gui/src/renderer/lib/utilityHooks.ts
+++ b/gui/src/renderer/lib/utilityHooks.ts
@@ -69,3 +69,12 @@ export function useNormalBridgeSettings() {
const bridgeSettings = useSelector((state) => state.settings.bridgeSettings);
return bridgeSettings.normal;
}
+
+// This hook returns a function that can be used to force a rerender of a component, and
+// additionally also returns a variable that can be used to trigger effects as a result. This is a
+// hack and should be avoided unless there are no better ways.
+export function useRerenderer(): [() => void, number] {
+ const [count, setCount] = useState(0);
+ const rerender = useCallback(() => setCount((count) => count + 1), []);
+ return [rerender, count];
+}
diff --git a/gui/src/shared/ipc-helpers.ts b/gui/src/shared/ipc-helpers.ts
index 27ad17e22a..0ff3ca2064 100644
--- a/gui/src/shared/ipc-helpers.ts
+++ b/gui/src/shared/ipc-helpers.ts
@@ -6,7 +6,7 @@ import { capitalize } from './string-helpers';
type Handler<T, R> = (callback: (arg: T) => R) => void;
type Sender<T, R> = (arg: T) => R;
type Notifier<T> = ((arg: T) => void) | undefined;
-type Listener<T> = (callback: (arg: T) => void) => void;
+type Listener<T> = (callback: (arg: T) => void) => () => void;
interface MainToRenderer<T> {
direction: 'main-to-renderer';
@@ -154,7 +154,9 @@ export function notifyRenderer<T>(): MainToRenderer<T> {
direction: 'main-to-renderer',
send: notifyRendererImpl,
receive: (event, ipcRenderer) => (fn: (value: T) => void) => {
- ipcRenderer.on(event, (_event, newState: T) => fn(newState));
+ const listener = (_event: unknown, newState: T) => fn(newState);
+ ipcRenderer.on(event, listener);
+ return () => ipcRenderer.off(event, listener);
},
};
}
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index b94fe0a701..d90957764e 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -128,6 +128,7 @@ export const ipcSchema = {
shape: notifyRenderer<IWindowShapeParameters>(),
focus: notifyRenderer<boolean>(),
macOsScrollbarVisibility: notifyRenderer<MacOsScrollbarVisibility>(),
+ scaleFactorChange: notifyRenderer<void>(),
},
navigation: {
reset: notifyRenderer<void>(),