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;