Skip to content

Commit 6596d42

Browse files
committed
better editing of text
resolves #36
1 parent f65ab22 commit 6596d42

1 file changed

Lines changed: 120 additions & 29 deletions

File tree

Flitro/Editor/ContextDetailsView.swift

Lines changed: 120 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,24 @@ struct ContextDetailsView: View {
120120

121121
@State private var isEditingTitle = false
122122
@State private var draftTitle = ""
123+
// New: clearer editing/hover/focus states for the title
124+
@State private var isHoveringTitle = false
125+
@FocusState private var titleFieldFocused: Bool
123126

124127
// New: Editing state for context items
125128
@State private var editingItemIndex: EditingIndex? = nil
126129

130+
// Force toolbar relayout when the context or title size category changes
131+
private var principalLayoutID: String {
132+
if isEditingTitle {
133+
// Important: do not depend on draftTitle length while editing to avoid TextField resets
134+
return "\(context.id)-e"
135+
} else {
136+
let bucket = max(1, min(8, context.name.count / 12)) // coarse buckets to reduce churn
137+
return "\(context.id)-v-\(bucket)"
138+
}
139+
}
140+
127141
var body: some View {
128142
ZStack {
129143
VStack(spacing: 0) {
@@ -271,49 +285,108 @@ struct ContextDetailsView: View {
271285
}
272286
}
273287
.toolbar {
274-
ToolbarItem(placement: .navigation) {
288+
// Make the title occupy the principal (expandable) area of the toolbar
289+
ToolbarItem(placement: .principal) {
275290
if isEditingTitle {
276-
TextField("Context Name", text: $draftTitle, onCommit: {
277-
isEditingTitle = false
278-
let trimmed = draftTitle.trimmingCharacters(in: .whitespaces)
279-
if !trimmed.isEmpty && trimmed != context.name {
280-
context.name = trimmed
281-
contextManager.saveContexts()
291+
HStack(spacing: 8) {
292+
TextField("Context Name", text: $draftTitle, onCommit: { commitTitle() })
293+
.font(.system(size: 17, weight: .bold))
294+
.textFieldStyle(PlainTextFieldStyle())
295+
.focused($titleFieldFocused)
296+
.onAppear {
297+
// Focus the field when entering edit mode; draftTitle is set when toggling edit mode
298+
DispatchQueue.main.async { self.titleFieldFocused = true }
299+
}
300+
.onExitCommand { cancelEditTitle() }
301+
.lineLimit(1)
302+
.allowsTightening(true)
303+
.minimumScaleFactor(0.5)
304+
.fixedSize(horizontal: false, vertical: true)
305+
.frame(maxWidth: .infinity, alignment: .leading)
306+
// Quick actions for explicit save/cancel
307+
Button { commitTitle() } label: {
308+
Image(systemName: "checkmark.circle.fill").foregroundColor(.accentColor)
282309
}
283-
})
284-
.font(.system(size: 17, weight: .bold))
285-
.textFieldStyle(PlainTextFieldStyle())
286-
.frame(minWidth: 120, maxWidth: 200)
287-
.onAppear { draftTitle = context.name }
288-
.onExitCommand { isEditingTitle = false }
289-
} else {
290-
Text(context.name)
291-
.font(.system(size: 17, weight: .bold))
292-
.onTapGesture {
293-
draftTitle = context.name
294-
isEditingTitle = true
310+
.buttonStyle(.plain)
311+
.help("Save title")
312+
Button { cancelEditTitle() } label: {
313+
Image(systemName: "xmark.circle.fill").foregroundColor(.secondary)
295314
}
315+
.buttonStyle(.plain)
316+
.help("Cancel editing")
317+
}
318+
.padding(.vertical, 4)
319+
.padding(.horizontal, 8)
320+
.background(
321+
RoundedRectangle(cornerRadius: 8)
322+
.fill(Color(NSColor.textBackgroundColor))
323+
)
324+
.overlay(
325+
RoundedRectangle(cornerRadius: 8)
326+
.stroke(Color.accentColor.opacity(0.6), lineWidth: 1)
327+
)
328+
// Allow the title editor to expand across available toolbar space
329+
.frame(minWidth: 140, maxWidth: .infinity, alignment: .leading)
330+
// Keep stable id logic
331+
.id(principalLayoutID)
332+
} else {
333+
HStack(spacing: 6) {
334+
Text(context.name)
335+
.font(.system(size: 17, weight: .bold))
336+
.lineLimit(2)
337+
.truncationMode(.middle)
338+
.allowsTightening(true)
339+
.minimumScaleFactor(0.5)
340+
.multilineTextAlignment(.leading)
341+
.fixedSize(horizontal: false, vertical: true)
342+
.frame(maxWidth: .infinity, alignment: .leading)
343+
.contentShape(Rectangle())
344+
.onTapGesture {
345+
draftTitle = context.name
346+
isEditingTitle = true
347+
}
348+
// Reserve space for the pencil to avoid layout shifts and only fade it
349+
Image(systemName: "pencil")
350+
.font(.system(size: 13))
351+
.foregroundColor(.secondary)
352+
.opacity(isHoveringTitle ? 1 : 0)
353+
.frame(width: 16, height: 16)
354+
}
355+
.padding(.vertical, 4)
356+
.padding(.horizontal, 8)
357+
.background(
358+
RoundedRectangle(cornerRadius: 8)
359+
.fill(isHoveringTitle ? Color.accentColor.opacity(0.08) : Color.clear)
360+
)
361+
.overlay(
362+
RoundedRectangle(cornerRadius: 8)
363+
.stroke(isHoveringTitle ? Color.accentColor.opacity(0.25) : Color.clear, lineWidth: 1)
364+
)
365+
// Allow the title to expand across available toolbar space
366+
.frame(minWidth: 140, maxWidth: .infinity, alignment: .leading)
367+
.onHover { hovering in
368+
isHoveringTitle = hovering
369+
}
370+
.help("Click to rename")
371+
.id(principalLayoutID)
296372
}
297373
}
298-
ToolbarItemGroup(placement: .automatic) {
374+
// Ensure action buttons stay on the trailing edge
375+
ToolbarItemGroup(placement: .primaryAction) {
299376
// Add dropdown menu for adding items (unchanged)
300377
Menu {
301378
Button("Application", action: { showAddAppDialog = true })
302379
Button("Document", action: { showAddDocumentDialog = true })
303380
Button("Browser Tab", action: { showAddBrowserTabDialog = true })
304381
Button("Shell Script", action: { showAddTerminalDialog = true })
305382
} label: {
306-
HStack(spacing: 6) {
307-
Image(systemName: "plus")
308-
Text("Add")
309-
}
310-
.font(.system(size: 16, weight: .medium))
383+
// HStack(spacing: 6) {
384+
Image(systemName: "plus")
385+
// Text("Add")
386+
// }
387+
// .font(.system(size: 16, weight: .medium))
311388
.help("Add Item")
312389
}
313-
Rectangle()
314-
.frame(width: 1, height: 24)
315-
.foregroundColor(Color.gray.opacity(0.3))
316-
.padding(.horizontal, 4)
317390
// Single context button
318391
ContextButton(context: context, contextManager: contextManager)
319392
}
@@ -343,6 +416,24 @@ struct ContextDetailsView: View {
343416
.listRowInsets(EdgeInsets())
344417
.background(Color.clear)
345418
}
419+
420+
// MARK: - Title helpers
421+
private func commitTitle() {
422+
isEditingTitle = false
423+
let trimmed = draftTitle.trimmingCharacters(in: .whitespacesAndNewlines)
424+
if !trimmed.isEmpty && trimmed != context.name {
425+
context.name = trimmed
426+
contextManager.saveContexts()
427+
} else {
428+
// Restore draft to the current name if unchanged
429+
draftTitle = context.name
430+
}
431+
}
432+
433+
private func cancelEditTitle() {
434+
draftTitle = context.name
435+
isEditingTitle = false
436+
}
346437
}
347438

348439
struct ContextButton: View {

0 commit comments

Comments
 (0)