Skip to content

Commit 5ba2dd7

Browse files
committed
Add live quote pane controls
1 parent 79ee474 commit 5ba2dd7

31 files changed

Lines changed: 793 additions & 41 deletions

PLUGINS.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,42 @@ function MyPane() {
490490
}
491491
```
492492

493+
### Pane quick settings
494+
495+
A pane can surface important toggle settings next to its title. Each quick setting references a `toggle` field from the pane's normal settings definition, so the header control and settings dialog share the same persisted value and update behavior.
496+
497+
```typescript
498+
ctx.registerPane({
499+
id: "live-prices",
500+
name: "Live Prices",
501+
component: LivePricesPane,
502+
defaultPosition: "right",
503+
quickSettings: [
504+
{ type: "toggle", key: "liveStreaming", icon: "zap" },
505+
],
506+
settings: (context) => ({
507+
values: {
508+
liveStreaming: context.settings.liveStreaming !== false,
509+
},
510+
fields: [
511+
{
512+
key: "liveStreaming",
513+
label: "Live streaming",
514+
description: "Stream updates continuously when enabled.",
515+
type: "toggle",
516+
},
517+
],
518+
}),
519+
});
520+
521+
function LivePricesPane() {
522+
const [liveStreaming] = usePaneSettingValue("liveStreaming", true);
523+
// Use liveStreaming to select continuous updates or a slower polling path.
524+
}
525+
```
526+
527+
Quick settings currently support toggle fields with the `zap` icon. Unknown keys and non-toggle fields are ignored.
528+
493529
### Events
494530

495531
Subscribe to and emit app events:

src/components/layout/detached-pane-shell.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Box, Text, useRendererHost, useUiCapabilities } from "../../ui";
1+
import { Box, Span, Text, useRendererHost, useUiCapabilities } from "../../ui";
22
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
33
import { t } from "../../i18n";
44
import { useShortcut, useViewport } from "../../react/input";
@@ -49,6 +49,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
4949
const instance = useAppSelector((state) => findPaneInstance(state.config.layout, desktopWindowBridge.paneId) ?? null);
5050
const paneDef = instance ? pluginRegistry.panes.get(instance.paneId) ?? null : null;
5151
const hasPaneSettings = !!instance && pluginRegistry.hasPaneSettings(instance.instanceId);
52+
const quickSettings = instance ? pluginRegistry.resolvePaneQuickSettings(instance.instanceId) : [];
5253
const titleState = useMemo(
5354
() => ({ config, paneState }) as Parameters<typeof getPaneDisplayTitle>[0],
5455
[config, paneState],
@@ -118,6 +119,16 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
118119
stopMouse(event);
119120
pluginRegistry.openPaneSettingsFn(desktopWindowBridge.paneId);
120121
}, [desktopWindowBridge.paneId, pluginRegistry]);
122+
const toggleQuickSetting = useCallback((key: string, event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
123+
stopMouse(event);
124+
focusPane();
125+
void pluginRegistry.togglePaneQuickSetting(desktopWindowBridge.paneId, key).catch((error) => {
126+
pluginRegistry.notify({
127+
body: error instanceof Error ? error.message : "Could not update pane setting.",
128+
type: "error",
129+
});
130+
});
131+
}, [desktopWindowBridge.paneId, focusPane, pluginRegistry]);
121132

122133
if (!instance || !paneDef) {
123134
return (
@@ -183,9 +194,36 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
183194
paddingRight={showWindowControls ? 0 : 1}
184195
style={{ position: "relative" }}
185196
>
186-
<Box flexGrow={1} minWidth={0} overflow="hidden">
197+
<Box minWidth={0} flexShrink={1} overflow="hidden">
187198
<Text fg={paneTitleText(focused, true, colors)} selectable={false} data-gloom-role="pane-title">{title}</Text>
188199
</Box>
200+
{quickSettings.map((setting) => (
201+
<Box
202+
key={setting.key}
203+
height={1}
204+
minWidth={20}
205+
paddingLeft={1}
206+
paddingRight={1}
207+
alignItems="center"
208+
justifyContent="center"
209+
className="electrobun-webkit-app-region-no-drag"
210+
data-gloom-role="pane-quick-setting"
211+
data-setting-key={setting.key}
212+
data-gloom-interactive="true"
213+
aria-label={`${setting.label}: ${setting.value ? "on" : "off"}`}
214+
aria-pressed={setting.value}
215+
title={`${setting.label}: ${setting.value ? "on" : "off"}`}
216+
style={{ cursor: "pointer" }}
217+
onMouseDown={(event: any) => toggleQuickSetting(setting.key, event)}
218+
>
219+
<Span style={{ display: "inline-flex", width: 12, height: 12, color: setting.value ? colors.warning : colors.textDim }}>
220+
<svg viewBox="0 0 12 12" width="12" height="12" fill="none" aria-hidden="true">
221+
<path d="M7.1 1.2 2.7 6.5h3.1l-.7 4.3 4.4-5.5H6.4l.7-4.1Z" fill="currentColor" />
222+
</svg>
223+
</Span>
224+
</Box>
225+
))}
226+
<Box flexGrow={1} minWidth={0} />
189227
{hasPaneSettings && (
190228
<Text
191229
fg={paneTitleText(focused, true, colors)}

src/components/layout/floating-pane.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Box, Text, useUiCapabilities } from "../../ui";
22
import type { ReactNode } from "react";
33
import { colors, floatingPaneBg } from "../../theme/colors";
44
import { PaneBodyFrame, getPaneWindowAttributes } from "./pane/frame";
5-
import { PaneHeader } from "./pane/header";
5+
import { PaneHeader, type PaneHeaderQuickSetting } from "./pane/header";
66
import { hasPaneFooterContent, PaneFooterBar, type CombinedPaneFooter } from "./pane/footer";
77
import { resolvePaneBodyFrame, shouldReservePaneFooter } from "./pane/sizing";
88

@@ -17,6 +17,7 @@ interface FloatingPaneWrapperProps {
1717
focused: boolean;
1818
windowModeSelected?: boolean;
1919
showActions?: boolean;
20+
quickSettings?: PaneHeaderQuickSetting[];
2021
onMouseDown?: (event: any) => void;
2122
onMouseDownCapture?: (event: any) => void;
2223
onHeaderMouseMove?: (event: any) => void;
@@ -63,6 +64,7 @@ export function FloatingPaneWrapper({
6364
focused,
6465
windowModeSelected = false,
6566
showActions = false,
67+
quickSettings,
6668
onMouseDown,
6769
onMouseDownCapture,
6870
onHeaderMouseMove,
@@ -115,6 +117,7 @@ export function FloatingPaneWrapper({
115117
windowModeSelected={windowModeSelected}
116118
floating
117119
showActions={showActions}
120+
quickSettings={quickSettings}
118121
onHeaderMouseMove={onHeaderMouseMove}
119122
onHeaderMouseDown={onHeaderMouseDown}
120123
onHeaderMouseDrag={onHeaderMouseDrag}

src/components/layout/pane/header.tsx

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface PaneHeaderProps {
1515
windowModeSelected?: boolean;
1616
floating?: boolean;
1717
showActions?: boolean;
18+
quickSettings?: PaneHeaderQuickSetting[];
1819
onHeaderMouseMove?: (event: any) => void;
1920
onHeaderMouseDown?: (event: any) => void;
2021
onHeaderMouseDrag?: (event: any) => void;
@@ -24,6 +25,15 @@ interface PaneHeaderProps {
2425
onCloseMouseDown?: (event: any) => void;
2526
}
2627

28+
export interface PaneHeaderQuickSetting {
29+
key: string;
30+
icon: "zap";
31+
label: string;
32+
description?: string;
33+
active: boolean;
34+
onMouseDown?: (event: any) => void;
35+
}
36+
2737
function truncateTitle(title: string, maxWidth: number): string {
2838
return truncateToDisplayWidth(title, maxWidth);
2939
}
@@ -43,9 +53,15 @@ function captureTerminalPointerDrag(renderer: unknown, renderable: unknown): voi
4353
function DesktopPaneButton({
4454
icon,
4555
onMouseDown,
56+
color = colors.textDim,
57+
label,
58+
pressed,
4659
}: {
4760
icon: ReactNode;
4861
onMouseDown?: (event: any) => void;
62+
color?: string;
63+
label?: string;
64+
pressed?: boolean;
4965
}) {
5066
return (
5167
<Box
@@ -54,6 +70,9 @@ function DesktopPaneButton({
5470
justifyContent="center"
5571
onMouseDown={onMouseDown}
5672
data-gloom-interactive={onMouseDown ? "true" : undefined}
73+
aria-label={label}
74+
aria-pressed={pressed}
75+
title={label}
5776
style={{
5877
borderRadius: 4,
5978
minWidth: 20,
@@ -69,7 +88,7 @@ function DesktopPaneButton({
6988
justifyContent: "center",
7089
width: 12,
7190
height: 12,
72-
color: colors.textDim,
91+
color,
7392
}}
7493
>
7594
{icon}
@@ -92,7 +111,7 @@ function TerminalPaneButton({
92111
return (
93112
<Box
94113
height={1}
95-
width={text.length}
114+
width={displayWidth(text)}
96115
flexDirection="row"
97116
data-gloom-role={role}
98117
data-gloom-interactive={onMouseDown ? "true" : undefined}
@@ -110,6 +129,7 @@ export function PaneHeader({
110129
windowModeSelected = false,
111130
floating = false,
112131
showActions = false,
132+
quickSettings = [],
113133
onHeaderMouseMove,
114134
onHeaderMouseDown,
115135
onHeaderMouseDrag,
@@ -125,6 +145,7 @@ export function PaneHeader({
125145
const backgroundColor = floating ? floatingPaneTitleBg(visuallyFocused) : paneTitleBg(visuallyFocused);
126146
const actionText = showActions ? PANE_HEADER_ACTION : " ";
127147
const closeText = floating ? PANE_HEADER_CLOSE : "";
148+
const terminalQuickSettingsWidth = quickSettings.reduce((total) => total + displayWidth(" ⚡ "), 0);
128149
const textColor = paneTitleText(visuallyFocused, floating);
129150
const handleTerminalHeaderMouseDown = useCallback((event: any) => {
130151
captureTerminalPointerDrag(nativeRenderer, terminalHeaderRef.current);
@@ -158,7 +179,7 @@ export function PaneHeader({
158179
<Text fg={visuallyFocused ? colors.borderFocused : colors.textMuted} selectable={false} data-gloom-role="pane-grip">
159180
{PANE_HEADER_GRIP}
160181
</Text>
161-
<Box flexGrow={1} minWidth={0} overflow="hidden">
182+
<Box minWidth={0} flexShrink={1} overflow="hidden">
162183
<Text
163184
fg={textColor}
164185
selectable={false}
@@ -173,6 +194,25 @@ export function PaneHeader({
173194
{title}
174195
</Text>
175196
</Box>
197+
{quickSettings.map((setting) => (
198+
<Box key={setting.key} data-gloom-role="pane-quick-setting" data-setting-key={setting.key}>
199+
<DesktopPaneButton
200+
onMouseDown={setting.onMouseDown}
201+
color={setting.active ? colors.warning : colors.textDim}
202+
label={`${setting.label}: ${setting.active ? "on" : "off"}`}
203+
pressed={setting.active}
204+
icon={(
205+
<svg viewBox="0 0 12 12" width="12" height="12" fill="none" aria-hidden="true">
206+
<path
207+
d="M7.1 1.2 2.7 6.5h3.1l-.7 4.3 4.4-5.5H6.4l.7-4.1Z"
208+
fill="currentColor"
209+
/>
210+
</svg>
211+
)}
212+
/>
213+
</Box>
214+
))}
215+
<Box flexGrow={1} minWidth={0} />
176216
<Box data-gloom-role="pane-action">
177217
{showActions ? (
178218
<DesktopPaneButton
@@ -213,10 +253,10 @@ export function PaneHeader({
213253
// Reserve 2 for corners, 1 for ─ after ┌, 1 for ─ before ┐
214254
const borderColor = visuallyFocused ? colors.borderFocused : colors.border;
215255
const innerWidth = Math.max(0, width - 4);
216-
const contentWidth = PANE_HEADER_GRIP.length + closeText.length + actionText.length;
256+
const contentWidth = PANE_HEADER_GRIP.length + terminalQuickSettingsWidth + closeText.length + actionText.length;
217257
const titleWidth = Math.max(0, innerWidth - contentWidth);
218258
const clippedTitle = truncateTitle(title, titleWidth);
219-
const fillLen = Math.max(0, innerWidth - PANE_HEADER_GRIP.length - displayWidth(clippedTitle) - actionText.length - closeText.length);
259+
const fillLen = Math.max(0, innerWidth - PANE_HEADER_GRIP.length - displayWidth(clippedTitle) - terminalQuickSettingsWidth - actionText.length - closeText.length);
220260
const fill = "─".repeat(fillLen);
221261

222262
return (
@@ -233,6 +273,15 @@ export function PaneHeader({
233273
>
234274
<Text fg={borderColor} selectable={false}>{"┌─"}</Text>
235275
<Text fg={textColor} selectable={false}>{`${PANE_HEADER_GRIP}${clippedTitle}`}</Text>
276+
{quickSettings.map((setting) => (
277+
<TerminalPaneButton
278+
key={setting.key}
279+
text=" ⚡ "
280+
fg={setting.active ? colors.warning : colors.textDim}
281+
role="pane-quick-setting"
282+
onMouseDown={setting.onMouseDown}
283+
/>
284+
))}
236285
<Text fg={borderColor} selectable={false}>{fill}</Text>
237286
<TerminalPaneButton
238287
text={actionText}
@@ -253,7 +302,7 @@ export function PaneHeader({
253302
);
254303
}
255304

256-
const titleWidth = Math.max(0, width - PANE_HEADER_GRIP.length - actionText.length - closeText.length);
305+
const titleWidth = Math.max(0, width - PANE_HEADER_GRIP.length - terminalQuickSettingsWidth - actionText.length - closeText.length);
257306
const clippedTitle = truncateTitle(title, titleWidth);
258307
const padding = " ".repeat(Math.max(0, titleWidth - displayWidth(clippedTitle)));
259308

@@ -272,6 +321,15 @@ export function PaneHeader({
272321
<Text fg={textColor} selectable={false}>
273322
{`${PANE_HEADER_GRIP}${clippedTitle}${padding}`}
274323
</Text>
324+
{quickSettings.map((setting) => (
325+
<TerminalPaneButton
326+
key={setting.key}
327+
text=" ⚡ "
328+
fg={setting.active ? colors.warning : colors.textDim}
329+
role="pane-quick-setting"
330+
onMouseDown={setting.onMouseDown}
331+
/>
332+
))}
275333
<TerminalPaneButton
276334
text={actionText}
277335
fg={textColor}

src/components/layout/pane/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Box, useUiCapabilities } from "../../../ui";
22
import type { ReactNode } from "react";
33
import { paneBg } from "../../../theme/colors";
44
import { PaneBodyFrame, getPaneWindowAttributes } from "./frame";
5-
import { PaneHeader } from "./header";
5+
import { PaneHeader, type PaneHeaderQuickSetting } from "./header";
66
import { hasPaneFooterContent, PaneFooterBar, type CombinedPaneFooter } from "./footer";
77
import { resolvePaneBodyFrame, shouldReservePaneFooter } from "./sizing";
88

@@ -15,6 +15,7 @@ interface PaneWrapperProps {
1515
height?: number | `${number}%` | "auto";
1616
flexGrow?: number;
1717
showActions?: boolean;
18+
quickSettings?: PaneHeaderQuickSetting[];
1819
onMouseDown?: (event: any) => void;
1920
onMouseDownCapture?: (event: any) => void;
2021
onHeaderMouseMove?: (event: any) => void;
@@ -36,6 +37,7 @@ export function PaneWrapper({
3637
height,
3738
flexGrow,
3839
showActions = false,
40+
quickSettings,
3941
onMouseDown,
4042
onMouseDownCapture,
4143
onHeaderMouseMove,
@@ -87,6 +89,7 @@ export function PaneWrapper({
8789
focused={focused}
8890
windowModeSelected={windowModeSelected}
8991
showActions={showActions}
92+
quickSettings={quickSettings}
9093
onHeaderMouseMove={onHeaderMouseMove}
9194
onHeaderMouseDown={onHeaderMouseDown}
9295
onHeaderMouseDrag={onHeaderMouseDrag}

src/components/layout/shell/index.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from "../../../state/selectors-ui";
3131
import { useThemeColors } from "../../../theme/theme-context";
3232
import { getPaneDisplayTitle } from "../pane/title";
33+
import type { PaneHeaderQuickSetting } from "../pane/header";
3334
import { getShortcutDisplayMode } from "../../../utils/shortcut-labels";
3435
import {
3536
actionMenuWidth,
@@ -423,6 +424,27 @@ export function Shell({
423424
(pane: ResolvedPane): string => getPaneDisplayTitle(titleState, pane.instance, pane.def),
424425
[titleState],
425426
);
427+
const handlePaneQuickSetting = useCallback((paneId: string, key: string, event: any) => {
428+
event?.preventDefault?.();
429+
event?.stopPropagation?.();
430+
focusPane(paneId);
431+
void pluginRegistry.togglePaneQuickSetting(paneId, key).catch((error) => {
432+
pluginRegistry.notify({
433+
body: error instanceof Error ? error.message : "Could not update pane setting.",
434+
type: "error",
435+
});
436+
});
437+
}, [focusPane, pluginRegistry]);
438+
const getPaneQuickSettings = useCallback((paneId: string): PaneHeaderQuickSetting[] => (
439+
pluginRegistry.resolvePaneQuickSettings(paneId).map((setting) => ({
440+
key: setting.key,
441+
icon: setting.icon,
442+
label: setting.label,
443+
description: setting.description,
444+
active: setting.value,
445+
onMouseDown: (event) => handlePaneQuickSetting(paneId, setting.key, event),
446+
}))
447+
), [config, handlePaneQuickSetting, pluginRegistry]);
426448

427449
const openPaneMenu = useCallback((paneId: string, rect: LayoutBounds, event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
428450
const pane = paneMap.get(paneId);
@@ -561,6 +583,7 @@ export function Shell({
561583
dragFloatingRect={dragFloatingRect}
562584
focusedPaneId={focusedPaneId}
563585
getPaneTitle={getPaneTitle}
586+
getPaneQuickSettings={getPaneQuickSettings}
564587
handleFloatingClose={handleFloatingClose}
565588
handleFloatingCloseMouseDown={handleFloatingCloseMouseDown}
566589
handleNativeDrag={handleNativeDrag}

0 commit comments

Comments
 (0)