Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ function MainApp() {
setAppSettings,
doctor,
appSettingsLoading,
reduceTransparency,
setReduceTransparency,
transparencyMode,
setTransparencyMode,
transparencyModes,
uiScale,
scaleShortcutTitle,
scaleShortcutText,
Expand Down Expand Up @@ -144,7 +145,7 @@ function MainApp() {
handleCopyDebug,
clearDebugEntries,
} = useDebugLog();
useLiquidGlassEffect({ reduceTransparency, onDebug: addDebugEntry });
useLiquidGlassEffect({ transparencyMode, onDebug: addDebugEntry });
const [accessMode, setAccessMode] = useState<AccessMode>("current");
const [activeTab, setActiveTab] = useState<
"projects" | "codex" | "git" | "log"
Expand Down Expand Up @@ -1275,7 +1276,7 @@ function MainApp() {
const appClassName = `app ${isCompact ? "layout-compact" : "layout-desktop"}${
isPhone ? " layout-phone" : ""
}${isTablet ? " layout-tablet" : ""}${
reduceTransparency ? " reduced-transparency" : ""
transparencyMode === "reduced" ? " reduced-transparency" : ""
}${!isCompact && sidebarCollapsed ? " sidebar-collapsed" : ""}${
!isCompact && rightPanelCollapsed ? " right-panel-collapsed" : ""
}${isDefaultScale ? " ui-scale-default" : ""}`;
Expand Down Expand Up @@ -1748,8 +1749,9 @@ function MainApp() {
onMoveWorkspaceGroup: moveWorkspaceGroup,
onDeleteWorkspaceGroup: deleteWorkspaceGroup,
onAssignWorkspaceGroup: assignWorkspaceGroup,
reduceTransparency,
onToggleTransparency: setReduceTransparency,
transparencyMode,
transparencyModes,
onTransparencyModeChange: setTransparencyMode,
appSettings,
onUpdateAppSettings: async (next) => {
await queueSaveSettings(next);
Expand Down
12 changes: 8 additions & 4 deletions src/features/app/hooks/useAppSettingsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ export function useAppSettingsController() {
} = useAppSettings();

useThemePreference(appSettings.theme);
const { reduceTransparency, setReduceTransparency } =
useTransparencyPreference();
const {
transparencyMode,
setTransparencyMode,
availableModes: transparencyModes,
} = useTransparencyPreference();

const {
uiScale,
Expand All @@ -34,8 +37,9 @@ export function useAppSettingsController() {
queueSaveSettings,
doctor,
appSettingsLoading,
reduceTransparency,
setReduceTransparency,
transparencyMode,
setTransparencyMode,
transparencyModes,
uiScale,
scaleShortcutTitle,
scaleShortcutText,
Expand Down
36 changes: 17 additions & 19 deletions src/features/app/hooks/useLiquidGlassEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import {
GlassMaterialVariant,
} from "tauri-plugin-liquid-glass-api";
import { Effect, EffectState, getCurrentWindow } from "@tauri-apps/api/window";
import type { DebugEntry } from "../../../types";
import type { DebugEntry, TransparencyMode } from "../../../types";

type Params = {
reduceTransparency: boolean;
transparencyMode: TransparencyMode;
onDebug?: (entry: DebugEntry) => void;
};

export function useLiquidGlassEffect({ reduceTransparency, onDebug }: Params) {
export function useLiquidGlassEffect({ transparencyMode, onDebug }: Params) {
const supportedRef = useRef<boolean | null>(null);

useEffect(() => {
Expand All @@ -21,24 +21,25 @@ export function useLiquidGlassEffect({ reduceTransparency, onDebug }: Params) {
const apply = async () => {
try {
const window = getCurrentWindow();
if (reduceTransparency) {
if (supportedRef.current === null) {
supportedRef.current = await isGlassSupported();
}
if (supportedRef.current) {
await setLiquidGlassEffect({ enabled: false });
}
await window.setEffects({ effects: [] });
return;
}

if (supportedRef.current === null) {
supportedRef.current = await isGlassSupported();
}
if (cancelled) {
return;
}
if (supportedRef.current) {
if (supportedRef.current && transparencyMode !== "glass") {
await setLiquidGlassEffect({ enabled: false });
}

const userAgent = navigator.userAgent ?? "";
const isMac = userAgent.includes("Macintosh");
const isLinux = userAgent.includes("Linux");
if (transparencyMode === "reduced") {
await window.setEffects({ effects: [] });
return;
}

if (supportedRef.current && transparencyMode === "glass") {
await window.setEffects({ effects: [] });
await setLiquidGlassEffect({
enabled: true,
Expand All @@ -48,9 +49,6 @@ export function useLiquidGlassEffect({ reduceTransparency, onDebug }: Params) {
return;
}

const userAgent = navigator.userAgent ?? "";
const isMac = userAgent.includes("Macintosh");
const isLinux = userAgent.includes("Linux");
if (!isMac && !isLinux) {
return;
}
Expand Down Expand Up @@ -78,5 +76,5 @@ export function useLiquidGlassEffect({ reduceTransparency, onDebug }: Params) {
return () => {
cancelled = true;
};
}, [onDebug, reduceTransparency]);
}, [onDebug, transparencyMode]);
}
83 changes: 73 additions & 10 deletions src/features/layout/hooks/useTransparencyPreference.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,80 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { isGlassSupported } from "tauri-plugin-liquid-glass-api";
import type { TransparencyMode } from "../../../types";

export function useTransparencyPreference(storageKey = "reduceTransparency") {
const [reduceTransparency, setReduceTransparency] = useState(() => {
const stored = localStorage.getItem(storageKey);
return stored === "true";
});
const STORAGE_KEY = "transparencyMode";
const LEGACY_STORAGE_KEY = "reduceTransparency";

const isTransparencyMode = (value: string | null): value is TransparencyMode =>
value === "glass" || value === "blur" || value === "reduced";

const readStoredTransparency = () => {
const stored = localStorage.getItem(STORAGE_KEY);
if (isTransparencyMode(stored)) {
return { mode: stored, hasStoredPreference: true };
}
const legacyStored = localStorage.getItem(LEGACY_STORAGE_KEY);
if (legacyStored === "true") {
return { mode: "reduced" as TransparencyMode, hasStoredPreference: true };
}
return { mode: "blur" as TransparencyMode, hasStoredPreference: false };
};

export function useTransparencyPreference() {
const [{ mode, hasStoredPreference }, setPreferenceState] = useState(() =>
readStoredTransparency(),
);
const [glassSupported, setGlassSupported] = useState<boolean | null>(null);

useEffect(() => {
localStorage.setItem(STORAGE_KEY, mode);
}, [mode]);

useEffect(() => {
localStorage.setItem(storageKey, String(reduceTransparency));
}, [reduceTransparency, storageKey]);
let cancelled = false;

const checkSupport = async () => {
try {
const supported = await isGlassSupported();
if (cancelled) {
return;
}
setGlassSupported(supported);
if (!supported && mode === "glass") {
setPreferenceState({ mode: "blur", hasStoredPreference });
}
if (!hasStoredPreference && supported && mode !== "glass") {
setPreferenceState({ mode: "glass", hasStoredPreference: false });
}
} catch {
if (!cancelled) {
setGlassSupported(false);
}
}
};

void checkSupport();

return () => {
cancelled = true;
};
}, [hasStoredPreference, mode]);

const setTransparencyMode = useCallback((next: TransparencyMode) => {
setPreferenceState({ mode: next, hasStoredPreference: true });
}, []);

const availableModes = useMemo<TransparencyMode[]>(() => {
if (glassSupported) {
return ["glass", "blur", "reduced"];
}
return ["blur", "reduced"];
}, [glassSupported]);

return {
reduceTransparency,
setReduceTransparency,
transparencyMode: mode,
setTransparencyMode,
availableModes,
glassSupported,
};
}
43 changes: 26 additions & 17 deletions src/features/settings/components/SettingsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,21 @@ const createDoctorResult = () => ({
const renderDisplaySection = (
options: {
appSettings?: Partial<AppSettings>;
reduceTransparency?: boolean;
transparencyMode?: ComponentProps<typeof SettingsView>["transparencyMode"];
transparencyModes?: ComponentProps<typeof SettingsView>["transparencyModes"];
onUpdateAppSettings?: ComponentProps<typeof SettingsView>["onUpdateAppSettings"];
onToggleTransparency?: ComponentProps<typeof SettingsView>["onToggleTransparency"];
onTransparencyModeChange?: ComponentProps<typeof SettingsView>["onTransparencyModeChange"];
} = {},
) => {
cleanup();
const onUpdateAppSettings =
options.onUpdateAppSettings ?? vi.fn().mockResolvedValue(undefined);
const onToggleTransparency = options.onToggleTransparency ?? vi.fn();
const onTransparencyModeChange =
options.onTransparencyModeChange ?? vi.fn();
const props: ComponentProps<typeof SettingsView> = {
reduceTransparency: options.reduceTransparency ?? false,
onToggleTransparency,
transparencyMode: options.transparencyMode ?? "blur",
transparencyModes: options.transparencyModes ?? ["blur", "reduced"],
onTransparencyModeChange,
appSettings: { ...baseSettings, ...options.appSettings },
onUpdateAppSettings,
workspaceGroups: [],
Expand Down Expand Up @@ -120,7 +123,7 @@ const renderDisplaySection = (
render(<SettingsView {...props} />);
fireEvent.click(screen.getByRole("button", { name: "Display & Sound" }));

return { onUpdateAppSettings, onToggleTransparency };
return { onUpdateAppSettings, onTransparencyModeChange };
};

describe("SettingsView Display", () => {
Expand All @@ -138,19 +141,23 @@ describe("SettingsView Display", () => {
});
});

it("toggles reduce transparency", () => {
const onToggleTransparency = vi.fn();
renderDisplaySection({ onToggleTransparency, reduceTransparency: false });
it("updates transparency mode", () => {
const onTransparencyModeChange = vi.fn();
renderDisplaySection({
onTransparencyModeChange,
transparencyMode: "blur",
transparencyModes: ["blur", "reduced"],
});

const row = screen
.getByText("Reduce transparency")
.getByText("Transparency")
.closest(".settings-toggle-row") as HTMLElement | null;
if (!row) {
throw new Error("Expected reduce transparency row");
throw new Error("Expected transparency row");
}
fireEvent.click(within(row).getByRole("button"));
fireEvent.click(within(row).getByRole("radio", { name: "Reduced" }));

expect(onToggleTransparency).toHaveBeenCalledWith(true);
expect(onTransparencyModeChange).toHaveBeenCalledWith("reduced");
});

it("commits interface scale on blur and enter with clamping", async () => {
Expand Down Expand Up @@ -280,8 +287,9 @@ describe("SettingsView Shortcuts", () => {
onMoveWorkspaceGroup={vi.fn().mockResolvedValue(null)}
onDeleteWorkspaceGroup={vi.fn().mockResolvedValue(null)}
onAssignWorkspaceGroup={vi.fn().mockResolvedValue(null)}
reduceTransparency={false}
onToggleTransparency={vi.fn()}
transparencyMode="blur"
transparencyModes={["blur", "reduced"]}
onTransparencyModeChange={vi.fn()}
appSettings={baseSettings}
onUpdateAppSettings={vi.fn().mockResolvedValue(undefined)}
onRunDoctor={vi.fn().mockResolvedValue(createDoctorResult())}
Expand Down Expand Up @@ -318,8 +326,9 @@ describe("SettingsView Shortcuts", () => {
onMoveWorkspaceGroup={vi.fn().mockResolvedValue(null)}
onDeleteWorkspaceGroup={vi.fn().mockResolvedValue(null)}
onAssignWorkspaceGroup={vi.fn().mockResolvedValue(null)}
reduceTransparency={false}
onToggleTransparency={vi.fn()}
transparencyMode="blur"
transparencyModes={["blur", "reduced"]}
onTransparencyModeChange={vi.fn()}
appSettings={baseSettings}
onUpdateAppSettings={vi.fn().mockResolvedValue(undefined)}
onRunDoctor={vi.fn().mockResolvedValue(createDoctorResult())}
Expand Down
Loading
Loading