diff --git a/apps/desktop/src/components/BranchList.svelte b/apps/desktop/src/components/BranchList.svelte
index 6b3ec645ea..0dc013ff5e 100644
--- a/apps/desktop/src/components/BranchList.svelte
+++ b/apps/desktop/src/components/BranchList.svelte
@@ -15,6 +15,8 @@
import { getColorFromCommitState, getIconFromCommitState } from '$components/lib';
import { STACKING_REORDER_DROPZONE_MANAGER_FACTORY } from '$lib/dragging/stackingReorderDropzoneManager';
import { editPatch } from '$lib/editMode/editPatchUtils';
+ import { branchFocusableId, stackFocusableId } from '$lib/focus/focusManager.svelte';
+ import { focusable } from '$lib/focus/focusable.svelte';
import { DEFAULT_FORGE_FACTORY } from '$lib/forge/forgeFactory.svelte';
import { INTELLIGENT_SCROLLING_SERVICE } from '$lib/intelligentScrolling/service';
import { MODE_SERVICE } from '$lib/mode/modeService';
@@ -138,190 +140,196 @@
: undefined}
{@const first = i === 0}
-
-
- {#snippet children([localAndRemoteCommits, upstreamOnlyCommits, branchDetails, commit])}
- {@const firstBranch = i === 0}
- {@const lastBranch = i === branches.length - 1}
- {@const iconName = getIconFromCommitState(commit?.id, commit?.state)}
- {@const lineColor = commit
- ? getColorFromCommitState(
- commit.state.type,
- commit.state.type === 'LocalAndRemote' && commit.id !== commit.state.subject
- )
- : 'var(--clr-commit-local)'}
- {@const isNewBranch =
- upstreamOnlyCommits.length === 0 && localAndRemoteCommits.length === 0}
- {@const selected =
- selection?.current?.branchName === branchName &&
- selection?.current.commitId === undefined}
- {@const pushStatus = branchDetails.pushStatus}
- {@const isConflicted = branchDetails.isConflicted}
- {@const lastUpdatedAt = branchDetails.lastUpdatedAt}
- {@const reviewId = branch.reviewId || undefined}
- {@const prNumber = branch.prNumber || undefined}
- {
- uiState.stack(stackId).selection.set({ branchName });
- intelligentScrollingService.show(projectId, stackId, 'details');
- onselect?.();
- }}
- >
- {#snippet buttons()}
-
+ {/snippet}
+
+
{/each}
diff --git a/apps/desktop/src/components/FocusCursor.svelte b/apps/desktop/src/components/FocusCursor.svelte
new file mode 100644
index 0000000000..d8883f0b0b
--- /dev/null
+++ b/apps/desktop/src/components/FocusCursor.svelte
@@ -0,0 +1,116 @@
+
+
+{#if target}
+
+{/if}
+
+
diff --git a/apps/desktop/src/components/MainViewport.svelte b/apps/desktop/src/components/MainViewport.svelte
index 7cbc73d20e..44c22b357c 100644
--- a/apps/desktop/src/components/MainViewport.svelte
+++ b/apps/desktop/src/components/MainViewport.svelte
@@ -173,7 +173,7 @@ the window, then enlarge it and retain the original widths of the layout.
style:width={finalPreviewWidth + 'rem'}
style:min-width={previewMinWidth + 'rem'}
use:focusable={{
- id: DefinedFocusable.ViewportMiddle,
+ id: DefinedFocusable.ViewportLeftExpanded,
parentId: DefinedFocusable.MainViewport
}}
>
diff --git a/apps/desktop/src/components/StackView.svelte b/apps/desktop/src/components/StackView.svelte
index a693c4cca3..d51d339fcb 100644
--- a/apps/desktop/src/components/StackView.svelte
+++ b/apps/desktop/src/components/StackView.svelte
@@ -12,7 +12,11 @@
import SelectionView from '$components/SelectionView.svelte';
import WorktreeChanges from '$components/WorktreeChanges.svelte';
import { isParsedError } from '$lib/error/parser';
- import { DefinedFocusable } from '$lib/focus/focusManager.svelte';
+ import {
+ DefinedFocusable,
+ newCommitAndAssignedFilesId,
+ stackFocusableId
+ } from '$lib/focus/focusManager.svelte';
import { focusable } from '$lib/focus/focusable.svelte';
import { DIFF_SERVICE } from '$lib/hunks/diffService.svelte';
import {
@@ -380,7 +384,7 @@
}
}}
use:focusable={{
- id: DefinedFocusable.Stack + ':' + stack.id,
+ id: stackFocusableId(stack.id),
parentId: DefinedFocusable.ViewportMiddle
}}
>
@@ -414,6 +418,10 @@
diff --git a/apps/desktop/src/lib/focus/focusManager.svelte.ts b/apps/desktop/src/lib/focus/focusManager.svelte.ts
index 70052b95e3..206b6d8ff1 100644
--- a/apps/desktop/src/lib/focus/focusManager.svelte.ts
+++ b/apps/desktop/src/lib/focus/focusManager.svelte.ts
@@ -9,6 +9,7 @@ export const FOCUS_MANAGER = new InjectionToken('FocusManager');
export enum DefinedFocusable {
MainViewport = 'workspace',
ViewportLeft = 'workspace-left',
+ ViewportLeftExpanded = 'workspace-left-expanded',
ViewportRight = 'workspace-right',
ViewportDrawerRight = 'workspace-drawer-right',
ViewportMiddle = 'workspace-middle',
@@ -17,8 +18,9 @@ export enum DefinedFocusable {
Branches = 'branches',
Stack = 'stack',
Preview = 'preview',
- // Only one of these can be in the dom at any given time.
- ChangedFiles = 'changed-files'
+ ChangedFiles = 'changed-files',
+ NewCommitAndAssignedFiles = 'new-commit-and-assigned-files',
+ Branch = 'branch'
}
export function stackFocusableId(stackId: string) {
@@ -29,6 +31,14 @@ export function uncommittedFocusableId(stackId?: string) {
return `${DefinedFocusable.UncommittedChanges}:${stackId}`;
}
+export function newCommitAndAssignedFilesId(stackId: string) {
+ return `${DefinedFocusable.NewCommitAndAssignedFiles}:${stackId}`;
+}
+
+export function branchFocusableId(stackId: string, branchName: string) {
+ return `${DefinedFocusable.Branch}:${stackId}:${branchName}`;
+}
+
/**
* If the provided ID is a assigned-changes id, it will return the stackId as a
* string, otherwise, it will return undefined.
@@ -50,6 +60,8 @@ export type FocusableElement = {
children: Focusable[];
};
+type Cursor = { setTarget(element: HTMLElement): void; show(): void };
+
/**
* Manages focusable areas through the `focusable` svelte action.
*
@@ -81,26 +93,42 @@ export class FocusManager implements Reactive {
private handleMouse = this.handleClick.bind(this);
private handleKeys = this.handleKeydown.bind(this);
+ private cursor: Cursor | undefined;
constructor() {
$effect(() => {
// We listen for events on the document in the bubble phase, giving
// other event handlers an opportunity to stop propagation.
+ console.log('I should happen _once_');
return mergeUnlisten(
on(document, 'click', this.handleMouse),
- on(document, 'keypress', this.handleKeys)
+ on(document, 'keydown', this.handleKeys)
);
});
}
+ setCursor(cursor: Cursor): () => void {
+ if (this.cursor) {
+ throw new Error('There should only ever be one instance of the focus cursor');
+ }
+
+ this.cursor = cursor;
+
+ return () => {
+ this.cursor = undefined;
+ };
+ }
+
get current() {
return this._current;
}
handleClick(e: Event) {
+ console.log('handling click');
if (e.target instanceof HTMLElement) {
let pointer: HTMLElement | null = e.target;
while (pointer) {
+ console.log('looping...');
const item = this.lookup.get(pointer);
if (item) {
this.setActive(item.key);
@@ -152,6 +180,10 @@ export class FocusManager implements Reactive {
setActive(id: Focusable) {
this._current = id;
+ const element = this.elements.find((e) => e.key === id);
+ if (element) {
+ this.cursor?.setTarget(element.element);
+ }
}
focusSibling(forward = true) {
@@ -169,7 +201,10 @@ export class FocusManager implements Reactive {
const nextIndex = (index + (forward ? 1 : siblings.length - 1)) % siblings.length;
this.setActive(siblings[nextIndex]!);
- this.elements.find((a) => a.key === siblings[nextIndex])?.element.focus();
+ const element = this.elements.find((a) => a.key === siblings[nextIndex])?.element;
+ element?.focus();
+ element?.scrollIntoView();
+ this.cursor?.show();
}
focusParent() {
@@ -179,11 +214,15 @@ export class FocusManager implements Reactive {
const area = this.elements.find((a) => a.key === currentId);
if (area?.parentId) {
this.setActive(area.parentId);
- this.elements.find((a) => a.key === area.parentId)?.element.focus();
+ const element = this.elements.find((a) => a.key === area.parentId)?.element;
+ element?.focus();
+ element?.scrollIntoView();
+ this.cursor?.show();
}
}
focusFirstChild() {
+ console.log('focused first child');
const currentId = this._current;
if (!currentId) return;
@@ -191,7 +230,10 @@ export class FocusManager implements Reactive {
if (area && area.children.length > 0) {
const firstChild = area.children[0];
this.setActive(firstChild!);
- this.elements.find((a) => a.key === firstChild)?.element.focus();
+ const element = this.elements.find((a) => a.key === firstChild)?.element;
+ element?.focus();
+ element?.scrollIntoView();
+ this.cursor?.show();
}
}
@@ -205,6 +247,12 @@ export class FocusManager implements Reactive {
} else if (event.metaKey && event.key === 'ArrowDown') {
event.preventDefault();
this.focusFirstChild();
+ } else if (event.metaKey && event.key === 'ArrowLeft') {
+ event.preventDefault();
+ this.focusSibling(false);
+ } else if (event.metaKey && event.key === 'ArrowRight') {
+ event.preventDefault();
+ this.focusSibling(true);
}
}
diff --git a/apps/desktop/src/routes/+layout.svelte b/apps/desktop/src/routes/+layout.svelte
index 8a03acfa05..8c98194c7e 100644
--- a/apps/desktop/src/routes/+layout.svelte
+++ b/apps/desktop/src/routes/+layout.svelte
@@ -6,6 +6,7 @@
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
import { page } from '$app/state';
import AppUpdater from '$components/AppUpdater.svelte';
+ import FocusCursor from '$components/FocusCursor.svelte';
import GlobalModal from '$components/GlobalModal.svelte';
import GlobalSettingsMenuAction from '$components/GlobalSettingsMenuAction.svelte';
import PromptModal from '$components/PromptModal.svelte';
@@ -35,6 +36,7 @@
import { DropzoneRegistry, DROPZONE_REGISTRY } from '$lib/dragging/registry';
import FeedFactory, { FEED_FACTORY } from '$lib/feed/feed';
import { FILE_SERVICE } from '$lib/files/fileService';
+ import { FOCUS_MANAGER, FocusManager } from '$lib/focus/focusManager.svelte';
import { DefaultForgeFactory, DEFAULT_FORGE_FACTORY } from '$lib/forge/forgeFactory.svelte';
import { GitHubClient, GITHUB_CLIENT } from '$lib/forge/github/githubClient';
import {
@@ -273,6 +275,9 @@
provide(RESIZE_SYNC, new ResizeSync());
provide(GIT_SERVICE, new GitService(data.tauri));
+ const focusManager = new FocusManager();
+ provide(FOCUS_MANAGER, focusManager);
+
const settingsService = data.settingsService;
const settingsStore = settingsService.appSettings;
@@ -356,6 +361,8 @@
{/if}
+
+