diff --git a/frontend/src/components/widgets/labels/TextLabel.svelte b/frontend/src/components/widgets/labels/TextLabel.svelte index b2bec856..ad9995e6 100644 --- a/frontend/src/components/widgets/labels/TextLabel.svelte +++ b/frontend/src/components/widgets/labels/TextLabel.svelte @@ -83,6 +83,7 @@ data-tooltip-description={tooltipDescription} data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined} for={forCheckbox !== undefined ? `checkbox-input-${forCheckbox}` : undefined} + on:dblclick bind:this={self} > diff --git a/frontend/src/components/window/Panel.svelte b/frontend/src/components/window/Panel.svelte index 3302c31d..8b93f644 100644 --- a/frontend/src/components/window/Panel.svelte +++ b/frontend/src/components/window/Panel.svelte @@ -36,6 +36,7 @@ export let clickAction: ((index: number) => void) | undefined = undefined; export let closeAction: ((index: number) => void) | undefined = undefined; export let reorderAction: ((oldIndex: number, newIndex: number) => void) | undefined = undefined; + export let renameAction: ((index: number, newName: string) => void) | undefined = undefined; export let emptySpaceAction: (() => void) | undefined = undefined; export let crossPanelDropAction: ((sourcePanelId: string, targetPanelId: string, insertIndex: number) => void) | undefined = undefined; export let groupDropAction: ((sourcePanelId: string, targetPanelId: string, insertIndex: number) => void) | undefined = undefined; @@ -50,6 +51,28 @@ let tabElements: (LayoutRow | undefined)[] = []; + // Document tab rename state + let editingNameTabIndex: number | undefined = undefined; + let editingNameText = ""; + let editingNameInputElement: HTMLInputElement | undefined = undefined; + + async function setEditingTabName(tabIndex: number, currentName: string) { + editingNameText = currentName; + editingNameTabIndex = tabIndex; + + await tick(); + + editingNameInputElement?.focus(); + editingNameInputElement?.select(); + } + + function commitEditingTabName(event: Event) { + if (editingNameTabIndex === undefined || !(event.target instanceof HTMLInputElement)) return; + + renameAction?.(editingNameTabIndex, event.target.value); + editingNameTabIndex = undefined; + } + // Tab drag-and-drop state let dragStartState: { tabIndex: number; pointerX: number; pointerY: number; isGroupDrag: boolean } | undefined = undefined; let dragging = false; @@ -392,9 +415,30 @@ bind:this={tabElements[tabIndex]} > - {tabLabel.name} + {#if editingNameTabIndex !== tabIndex} + renameAction && setEditingTabName(tabIndex, tabLabel.name)}>{tabLabel.name} + {:else} + { + // Stop propagation when we handle the key ourselves so the global keyboard forwarder doesn't dispatch them and trigger unrelated bindings + if (e.key === "Enter") { + commitEditingTabName(e); + e.stopPropagation(); + } else if (e.key === "Escape") { + editingNameTabIndex = undefined; + e.stopPropagation(); + } + }} + /> + {/if} {#if tabLabel.unsaved} - * + * {/if} {#if tabCloseButtons} @@ -513,11 +557,31 @@ &.text { overflow-x: hidden; - white-space: nowrap; + white-space: pre; text-overflow: ellipsis; flex-shrink: 1; } } + + input { + color: inherit; + border: none; + outline: none; + margin: 0 -4px; + padding: 0 4px; + height: 20px; + border-radius: 2px; + background: var(--color-1-nearblack); + field-sizing: content; + align-self: center; + // Stack above the absolutely-positioned close button so it doesn't intercept clicks at the input's right edge. + position: relative; + z-index: 1; + } + + .text-label.hidden { + visibility: hidden; + } } .icon-button { diff --git a/frontend/src/components/window/PanelSubdivision.svelte b/frontend/src/components/window/PanelSubdivision.svelte index 0bd2d67b..1c764704 100644 --- a/frontend/src/components/window/PanelSubdivision.svelte +++ b/frontend/src/components/window/PanelSubdivision.svelte @@ -180,6 +180,11 @@ clickAction={(tabIndex) => editor.selectDocument($portfolio.documents[tabIndex].id)} closeAction={(tabIndex) => editor.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)} reorderAction={(oldIndex, newIndex) => editor.reorderDocument($portfolio.documents[oldIndex].id, newIndex)} + renameAction={(tabIndex, newName) => { + // Ensure the target document is the active one before renaming, since `RenameDocument` operates on the active document + editor.selectDocument($portfolio.documents[tabIndex].id); + editor.renameDocument(newName); + }} tabActiveIndex={$portfolio.activeDocumentIndex} groupDropAction={groupDrop} splitDropAction={splitDrop} diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 5e8b04da..eeb89caa 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -395,7 +395,7 @@ impl EditorWrapper { pub fn load_document_content(&self, document_id: u64, document: String) { let message = PersistentStateMessage::LoadDocument { document_id: DocumentId(document_id), - document: document, + document, }; self.dispatch(message); } @@ -407,6 +407,13 @@ impl EditorWrapper { self.dispatch(message); } + /// Rename the currently active document. + #[wasm_bindgen(js_name = renameDocument)] + pub fn rename_document(&self, new_name: String) { + let message = DocumentMessage::RenameDocument { new_name }; + self.dispatch(message); + } + #[wasm_bindgen(js_name = newDocumentDialog)] pub fn new_document_dialog(&self) { let message = DialogMessage::RequestNewDocumentDialog;