mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 00:44:49 +00:00
fix(app): keep every floating panel host registered
Anchored panels resolve their portal host by name. Several screens can mount a host under the same name, but the registry kept only the newest element: when that element unmounted, its cleanup deleted the entry, and the surviving host was no longer measurable. Every anchored panel then returned null with no error and no log, until something remounted the screen or reloaded the window. Track all hosts per name and measure the newest laid-out one, so unmounting a host only removes that host. Measurement now skips hosts that report no size, which covers retained copies that are still mounted but not laid out.
This commit is contained in:
committed by
paseo-bot[bot]
parent
1efcb31a6c
commit
61c9e803f1
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createFloatingPanelHosts,
|
||||
type FloatingPanelHostRect,
|
||||
type MeasurableFloatingPanelHost,
|
||||
} from "./floating-panel-hosts";
|
||||
|
||||
function createHost(rect: FloatingPanelHostRect): MeasurableFloatingPanelHost {
|
||||
return {
|
||||
measureInWindow: (callback) => callback(rect.x, rect.y, rect.width, rect.height),
|
||||
};
|
||||
}
|
||||
|
||||
const WORKSPACE_RECT: FloatingPanelHostRect = { x: 10, y: 20, width: 800, height: 600 };
|
||||
const RETAINED_VIEWPORT: FloatingPanelHostRect = { x: 0, y: 0, width: 1200, height: 900 };
|
||||
|
||||
describe("floating panel hosts", () => {
|
||||
it("measures the newest host registered under the name", async () => {
|
||||
const hosts = createFloatingPanelHosts();
|
||||
hosts.register("workspace", createHost(WORKSPACE_RECT));
|
||||
hosts.register("workspace", createHost(RETAINED_VIEWPORT));
|
||||
|
||||
expect(await hosts.measure("workspace")).toEqual(RETAINED_VIEWPORT);
|
||||
});
|
||||
|
||||
it("keeps measuring a survivor after the newest host unregisters", async () => {
|
||||
const hosts = createFloatingPanelHosts();
|
||||
const unregisterSurvivor = hosts.register("workspace", createHost(WORKSPACE_RECT));
|
||||
const unregisterNewest = hosts.register("workspace", createHost(RETAINED_VIEWPORT));
|
||||
|
||||
unregisterNewest();
|
||||
|
||||
expect(await hosts.measure("workspace")).toEqual(WORKSPACE_RECT);
|
||||
unregisterSurvivor();
|
||||
});
|
||||
|
||||
it("returns null once every host under the name is gone", async () => {
|
||||
const hosts = createFloatingPanelHosts();
|
||||
const unregisterFirst = hosts.register("workspace", createHost(WORKSPACE_RECT));
|
||||
const unregisterSecond = hosts.register("workspace", createHost(RETAINED_VIEWPORT));
|
||||
|
||||
unregisterFirst();
|
||||
unregisterSecond();
|
||||
|
||||
expect(await hosts.measure("workspace")).toBeNull();
|
||||
});
|
||||
|
||||
it("skips hosts that are not laid out", async () => {
|
||||
const hosts = createFloatingPanelHosts();
|
||||
hosts.register("workspace", createHost(WORKSPACE_RECT));
|
||||
hosts.register("workspace", createHost({ x: 0, y: 0, width: 0, height: 0 }));
|
||||
|
||||
expect(await hosts.measure("workspace")).toEqual(WORKSPACE_RECT);
|
||||
});
|
||||
|
||||
it("ignores a repeated unregister of the same host", async () => {
|
||||
const hosts = createFloatingPanelHosts();
|
||||
const unregister = hosts.register("workspace", createHost(WORKSPACE_RECT));
|
||||
|
||||
unregister();
|
||||
unregister();
|
||||
|
||||
expect(await hosts.measure("workspace")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
export interface FloatingPanelHostRect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface MeasurableFloatingPanelHost {
|
||||
measureInWindow(callback: (x: number, y: number, width: number, height: number) => void): void;
|
||||
}
|
||||
|
||||
export interface FloatingPanelHosts<T extends MeasurableFloatingPanelHost> {
|
||||
register(name: string, host: T): () => void;
|
||||
measure(name: string): Promise<FloatingPanelHostRect | null>;
|
||||
}
|
||||
|
||||
function measureHost(host: MeasurableFloatingPanelHost): Promise<FloatingPanelHostRect> {
|
||||
return new Promise((resolve) => {
|
||||
host.measureInWindow((x, y, width, height) => resolve({ x, y, width, height }));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts are addressed by name, and one name can have several live hosts: a workspace
|
||||
* screen mounts its own host while retained copies of that screen stay mounted. Track
|
||||
* every host so unmounting one leaves the others measurable.
|
||||
*/
|
||||
export function createFloatingPanelHosts<
|
||||
T extends MeasurableFloatingPanelHost,
|
||||
>(): FloatingPanelHosts<T> {
|
||||
const hostsByName = new Map<string, T[]>();
|
||||
|
||||
function register(name: string, host: T): () => void {
|
||||
const hosts = hostsByName.get(name) ?? [];
|
||||
hostsByName.set(name, hosts);
|
||||
hosts.push(host);
|
||||
|
||||
return () => {
|
||||
const index = hosts.indexOf(host);
|
||||
if (index === -1) return;
|
||||
hosts.splice(index, 1);
|
||||
if (hosts.length === 0 && hostsByName.get(name) === hosts) {
|
||||
hostsByName.delete(name);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function measure(name: string): Promise<FloatingPanelHostRect | null> {
|
||||
const hosts = hostsByName.get(name);
|
||||
if (!hosts || hosts.length === 0) return null;
|
||||
|
||||
const rects = await Promise.all(hosts.toReversed().map(measureHost));
|
||||
return rects.find((rect) => rect.width > 0 && rect.height > 0) ?? rects[0];
|
||||
}
|
||||
|
||||
return { register, measure };
|
||||
}
|
||||
@@ -8,32 +8,21 @@ import {
|
||||
} from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { PortalHost } from "@gorhom/portal";
|
||||
import {
|
||||
createFloatingPanelHosts,
|
||||
type FloatingPanelHostRect,
|
||||
} from "@/components/ui/floating-panel-hosts";
|
||||
|
||||
export const DEFAULT_FLOATING_PANEL_PORTAL_HOST = "content-floating-panels";
|
||||
|
||||
const FloatingPanelPortalHostNameContext = createContext(DEFAULT_FLOATING_PANEL_PORTAL_HOST);
|
||||
|
||||
interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
const portalHosts = createFloatingPanelHosts<View>();
|
||||
|
||||
const portalHostElements = new Map<string, View>();
|
||||
|
||||
function measureElement(element: View): Promise<Rect> {
|
||||
return new Promise((resolve) => {
|
||||
element.measureInWindow((x, y, width, height) => {
|
||||
resolve({ x, y, width, height });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function measureFloatingPanelPortalHost(name: string): Promise<Rect | null> {
|
||||
const element = portalHostElements.get(name);
|
||||
if (!element) return Promise.resolve(null);
|
||||
return measureElement(element);
|
||||
export function measureFloatingPanelPortalHost(
|
||||
name: string,
|
||||
): Promise<FloatingPanelHostRect | null> {
|
||||
return portalHosts.measure(name);
|
||||
}
|
||||
|
||||
export function FloatingPanelPortalHostNameProvider({
|
||||
@@ -64,12 +53,7 @@ export function FloatingPanelPortalHost({
|
||||
useEffect(() => {
|
||||
const host = hostRef.current;
|
||||
if (!host) return undefined;
|
||||
portalHostElements.set(name, host);
|
||||
return () => {
|
||||
if (portalHostElements.get(name) === host) {
|
||||
portalHostElements.delete(name);
|
||||
}
|
||||
};
|
||||
return portalHosts.register(name, host);
|
||||
}, [name]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user