fix(desktop): review fixes for workspace management UX

- Remove consumed flag (stale on re-open, double-fire is harmless)
- Fix keyboard nav for WORKSPACES tab (was broken, only worked for screens)
- Reset selectedIndex on tab switch
- Fix delete active workspace → reload columns for new active workspace
- Extract DeckColumnType.param() extension to eliminate 3x duplication
- Simplify moveSelection to handle all modes (tabs + unified search)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-04-17 14:55:25 +03:00
parent 09f76f036a
commit ac349665eb
4 changed files with 67 additions and 67 deletions
@@ -98,6 +98,7 @@ import com.vitorpamplona.amethyst.desktop.ui.deck.SinglePaneLayout
import com.vitorpamplona.amethyst.desktop.ui.deck.SinglePaneState import com.vitorpamplona.amethyst.desktop.ui.deck.SinglePaneState
import com.vitorpamplona.amethyst.desktop.ui.deck.Workspace import com.vitorpamplona.amethyst.desktop.ui.deck.Workspace
import com.vitorpamplona.amethyst.desktop.ui.deck.WorkspaceManager import com.vitorpamplona.amethyst.desktop.ui.deck.WorkspaceManager
import com.vitorpamplona.amethyst.desktop.ui.deck.param
import com.vitorpamplona.amethyst.desktop.ui.media.LocalAwtWindow import com.vitorpamplona.amethyst.desktop.ui.media.LocalAwtWindow
import com.vitorpamplona.amethyst.desktop.ui.media.LocalIsImmersiveFullscreen import com.vitorpamplona.amethyst.desktop.ui.media.LocalIsImmersiveFullscreen
import com.vitorpamplona.amethyst.desktop.ui.media.LocalWindowState import com.vitorpamplona.amethyst.desktop.ui.media.LocalWindowState
@@ -256,15 +257,7 @@ fun main() {
deckState.columns.value.map { col -> deckState.columns.value.map { col ->
Workspace.WorkspaceColumn( Workspace.WorkspaceColumn(
typeKey = col.type.typeKey(), typeKey = col.type.typeKey(),
param = param = col.type.param(),
when (col.type) {
is DeckColumnType.Hashtag -> col.type.tag
is DeckColumnType.Editor -> col.type.draftSlug
is DeckColumnType.Article -> col.type.addressTag
is DeckColumnType.Profile -> col.type.pubKeyHex
is DeckColumnType.Thread -> col.type.noteId
else -> null
},
width = col.width, width = col.width,
) )
} }
@@ -155,6 +155,16 @@ fun DeckColumnType.requiresInput(): Boolean =
else -> false else -> false
} }
fun DeckColumnType.param(): String? =
when (this) {
is DeckColumnType.Profile -> pubKeyHex
is DeckColumnType.Thread -> noteId
is DeckColumnType.Hashtag -> tag
is DeckColumnType.Editor -> draftSlug
is DeckColumnType.Article -> addressTag
else -> null
}
val LAUNCHABLE_SCREENS: List<DeckColumnType> = val LAUNCHABLE_SCREENS: List<DeckColumnType> =
listOf( listOf(
DeckColumnType.HomeFeed, DeckColumnType.HomeFeed,
@@ -189,7 +199,6 @@ private class AppDrawerState {
var activeTab by mutableStateOf(AppDrawerTab.SCREENS) var activeTab by mutableStateOf(AppDrawerTab.SCREENS)
var hashtagInput by mutableStateOf("") var hashtagInput by mutableStateOf("")
var awaitingHashtag by mutableStateOf(false) var awaitingHashtag by mutableStateOf(false)
private var consumed by mutableStateOf(false)
val filteredScreens: List<DeckColumnType> by derivedStateOf { val filteredScreens: List<DeckColumnType> by derivedStateOf {
if (searchQuery.isBlank()) { if (searchQuery.isBlank()) {
@@ -223,17 +232,22 @@ private class AppDrawerState {
} }
} }
fun moveSelection(delta: Int) { fun switchTab(tab: AppDrawerTab) {
val size = filteredScreens.size activeTab = tab
if (size > 0) selectedIndex = (selectedIndex + delta).coerceIn(0, size - 1) selectedIndex = 0
} }
fun moveSelectionUnified( fun moveSelection(
delta: Int, delta: Int,
wsCount: Int, wsCount: Int = 0,
) { ) {
val total = wsCount + filteredScreens.size val size =
if (total > 0) selectedIndex = (selectedIndex + delta).coerceIn(0, total - 1) when {
isSearching -> wsCount + filteredScreens.size
activeTab == AppDrawerTab.WORKSPACES -> wsCount
else -> filteredScreens.size
}
if (size > 0) selectedIndex = (selectedIndex + delta).coerceIn(0, size - 1)
} }
fun select( fun select(
@@ -241,12 +255,10 @@ private class AppDrawerState {
onSelectScreen: (DeckColumnType) -> Unit, onSelectScreen: (DeckColumnType) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
if (consumed) return
if (screen.requiresInput()) { if (screen.requiresInput()) {
awaitingHashtag = true awaitingHashtag = true
hashtagInput = "" hashtagInput = ""
} else { } else {
consumed = true
onSelectScreen(screen) onSelectScreen(screen)
onDismiss() onDismiss()
} }
@@ -258,21 +270,29 @@ private class AppDrawerState {
onSwitchWorkspace: (Workspace) -> Unit, onSwitchWorkspace: (Workspace) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
if (consumed) return if (isSearching) {
if (!isSearching) { // Unified: workspaces first, then screens
// Tab-based: only screens use Enter val wsCount = filteredWs.size
filteredScreens.getOrNull(selectedIndex)?.let { select(it, onSelectScreen, onDismiss) } if (selectedIndex < wsCount) {
return onSwitchWorkspace(filteredWs[selectedIndex])
} onDismiss()
// Unified: workspaces first, then screens } else {
val wsCount = filteredWs.size val screenIdx = selectedIndex - wsCount
if (selectedIndex < wsCount) { filteredScreens.getOrNull(screenIdx)?.let { select(it, onSelectScreen, onDismiss) }
consumed = true }
onSwitchWorkspace(filteredWs[selectedIndex])
onDismiss()
} else { } else {
val screenIdx = selectedIndex - wsCount when (activeTab) {
filteredScreens.getOrNull(screenIdx)?.let { select(it, onSelectScreen, onDismiss) } AppDrawerTab.SCREENS -> {
filteredScreens.getOrNull(selectedIndex)?.let { select(it, onSelectScreen, onDismiss) }
}
AppDrawerTab.WORKSPACES -> {
filteredWs.getOrNull(selectedIndex)?.let {
onSwitchWorkspace(it)
onDismiss()
}
}
}
} }
} }
@@ -280,8 +300,7 @@ private class AppDrawerState {
onSelectScreen: (DeckColumnType) -> Unit, onSelectScreen: (DeckColumnType) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
if (consumed || hashtagInput.isBlank()) return if (hashtagInput.isBlank()) return
consumed = true
onSelectScreen(DeckColumnType.Hashtag(hashtagInput.removePrefix("#").trim())) onSelectScreen(DeckColumnType.Hashtag(hashtagInput.removePrefix("#").trim()))
onDismiss() onDismiss()
} }
@@ -329,20 +348,12 @@ fun AppDrawer(
} }
Key.DirectionDown -> { Key.DirectionDown -> {
if (state.isSearching) { state.moveSelection(1, filteredWs.size)
state.moveSelectionUnified(1, filteredWs.size)
} else {
state.moveSelection(1)
}
true true
} }
Key.DirectionUp -> { Key.DirectionUp -> {
if (state.isSearching) { state.moveSelection(-1, filteredWs.size)
state.moveSelectionUnified(-1, filteredWs.size)
} else {
state.moveSelection(-1)
}
true true
} }
@@ -419,7 +430,7 @@ fun AppDrawer(
AppDrawerTab.entries.forEach { tab -> AppDrawerTab.entries.forEach { tab ->
Tab( Tab(
selected = state.activeTab == tab, selected = state.activeTab == tab,
onClick = { state.activeTab = tab }, onClick = { state.switchTab(tab) },
text = { text = {
Text( Text(
tab.name.lowercase().replaceFirstChar { it.uppercase() }, tab.name.lowercase().replaceFirstChar { it.uppercase() },
@@ -704,7 +715,13 @@ private fun WorkspacesGrid(
editTarget = ws editTarget = ws
showEditor = true showEditor = true
}, },
onDelete = { workspaceManager.deleteWorkspace(ws.id) }, onDelete = {
val wasActive = index == activeIndex
workspaceManager.deleteWorkspace(ws.id)
if (wasActive) {
onSwitchWorkspace(workspaceManager.activeWorkspace)
}
},
canDelete = workspaces.size > 1, canDelete = workspaces.size > 1,
) )
} }
@@ -1082,8 +1099,14 @@ private fun UnifiedSearchResults(
onDismiss() onDismiss()
} }
}, },
onEdit = { /* not supported in search results */ }, onEdit = { /* edit via Workspaces tab */ },
onDelete = { workspaceManager.deleteWorkspace(ws.id) }, onDelete = {
val wasActive = wsIdx == activeIndex
workspaceManager.deleteWorkspace(ws.id)
if (wasActive) {
onSwitchWorkspace(workspaceManager.activeWorkspace)
}
},
canDelete = allWorkspaces.size > 1, canDelete = allWorkspaces.size > 1,
) )
} }
@@ -221,15 +221,7 @@ class DeckState(
"id" to col.id, "id" to col.id,
"type" to col.type.typeKey(), "type" to col.type.typeKey(),
"width" to col.width, "width" to col.width,
"param" to "param" to col.type.param(),
when (col.type) {
is DeckColumnType.Profile -> col.type.pubKeyHex
is DeckColumnType.Thread -> col.type.noteId
is DeckColumnType.Hashtag -> col.type.tag
is DeckColumnType.Editor -> col.type.draftSlug
is DeckColumnType.Article -> col.type.addressTag
else -> null
},
) )
} }
DesktopPreferences.deckColumns = mapper.writeValueAsString(data) DesktopPreferences.deckColumns = mapper.writeValueAsString(data)
@@ -63,15 +63,7 @@ class WorkspaceManager(
columns.map { col -> columns.map { col ->
Workspace.WorkspaceColumn( Workspace.WorkspaceColumn(
typeKey = col.type.typeKey(), typeKey = col.type.typeKey(),
param = param = col.type.param(),
when (col.type) {
is DeckColumnType.Profile -> col.type.pubKeyHex
is DeckColumnType.Thread -> col.type.noteId
is DeckColumnType.Hashtag -> col.type.tag
is DeckColumnType.Editor -> col.type.draftSlug
is DeckColumnType.Article -> col.type.addressTag
else -> null
},
width = col.width, width = col.width,
) )
}, },