Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 40 additions & 23 deletions Cotabby/UI/Settings/Panes/WritingPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,40 +70,57 @@ struct WritingPaneView: View {
.settingsItem(.addSpaceAfterAccept)
}

Section("Corrections") {
// Typo handling is a dependency chain, so the UI discloses it progressively rather than
// showing every control at once. The master gate ("Hide Suggestions on Typo") detects
// misspellings; the correction actions only function while it is on; the dictionaries
// only matter once an action can consume them. Revealing each level when it becomes
// relevant — instead of dimming dependents in place — makes the hierarchy unmistakable:
// turning the gate off removes the controls that depend on it. This is the established
// master/dependent idiom in this app (see `EmojiPaneView`). The earlier flat layout put
// all three toggles at the same visual level, so it was not obvious the gate governed the
// other two.
Section("Typos") {
Toggle(isOn: suppressCompletionsOnTypoBinding) {
SettingsRowLabel(
title: "Hide Suggestions on Typo",
description: "Stops normal completions while the current word appears misspelled.",
description: "Pauses normal completions while the current word looks misspelled. " +
"Turn this on to use the correction options.",
systemImage: "eye.slash"
)
}
.settingsItem(.hideSuggestionsOnTypo)
}

Toggle(isOn: offerTypoCorrectionsBinding) {
SettingsRowLabel(
title: "Offer Corrections on Typo",
description: "Shows a green replacement you can apply with your accept key.",
systemImage: "checkmark.bubble"
)
}
.disabled(!suggestionSettings.suppressCompletionsOnTypo)
.settingsItem(.offerTypoCorrections)
if suggestionSettings.suppressCompletionsOnTypo {
Section("Corrections") {
Toggle(isOn: offerTypoCorrectionsBinding) {
SettingsRowLabel(
title: "Offer Corrections on Typo",
description: "Shows a green replacement you can apply with your accept key.",
systemImage: "checkmark.bubble"
)
}
.settingsItem(.offerTypoCorrections)

Toggle(isOn: automaticallyFixTyposBinding) {
SettingsRowLabel(
title: "Automatically Fix Typos",
description: "After you press Space, replaces a misspelled word without requiring your accept key.",
systemImage: "checkmark.circle"
)
Toggle(isOn: automaticallyFixTyposBinding) {
SettingsRowLabel(
title: "Automatically Fix Typos",
description: "After you press Space, replaces a misspelled word without requiring your accept key.",
systemImage: "checkmark.circle"
)
}
.settingsItem(.automaticallyFixTypos)
}
.disabled(!suggestionSettings.suppressCompletionsOnTypo)
.settingsItem(.automaticallyFixTypos)

Divider()

SpellingDictionaryPicker(suggestionSettings: suggestionSettings)
.settingsItem(.spellingDictionaries)
// Dictionaries rank candidates for the two correction actions above, so they only
// appear once at least one action is on. With neither active, choosing a dictionary
// would have no observable effect.
if suggestionSettings.offerTypoCorrections || suggestionSettings.automaticallyFixTypos {
Section("Spelling Dictionaries") {
SpellingDictionaryPicker(suggestionSettings: suggestionSettings)
.settingsItem(.spellingDictionaries)
}
}
Comment on lines +94 to +123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Search-navigation silently drops when dependent rows are hidden

When suppressCompletionsOnTypo is false, the .offerTypoCorrections, .automaticallyFixTypos, and .spellingDictionaries anchors are not in the view hierarchy. If the user finds one of those items via Settings search while the gate is off, SettingsNavigationModel.reveal(_:) correctly switches to the Writing pane, but SettingsPaneScaffold's scrollTo call targets a non-existent anchor and silently no-ops — no scroll and no arrival pulse. The user lands on the pane with nothing highlighted and no indication of why.

With the old flat layout, all three rows were always rendered (just .disabled()), so the scroll target was always present. This PR removes that guarantee. The same edge case exists today in EmojiPaneView for emoji sub-settings, so this is consistent with the existing pattern — but it's worth noting since the typo sub-settings are more likely to be searched independently (e.g., a user searching "Spelling Dictionaries" to choose a language before realising the gate exists).

A lightweight mitigation used elsewhere is to gate search result surfacing on the dependent condition, or to auto-enable the gate when search navigates directly to a dependent item.

Fix in Codex Fix in Claude Code

}

Section("Profile") {
Expand Down
20 changes: 5 additions & 15 deletions Cotabby/UI/SpellingDictionaryPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,16 @@ import SwiftUI
/// This view owns presentation only. `SuggestionSettingsModel` normalizes and persists each toggle,
/// while `SpellingLanguageResolver` and `SymSpellCorrector` own runtime selection and loading. Keeping
/// those responsibilities separate prevents a Settings view from constructing heavyweight indexes.
///
/// Relevance gating lives in the parent (`WritingPaneView`): the picker is only rendered once the
/// typo gate and at least one correction action are on, so the checkboxes here are always live and
/// carry no self-disabling logic. The enclosing `Section("Spelling Dictionaries")` supplies the
/// header, so this view starts straight at its explanatory caption.
struct SpellingDictionaryPicker: View {
@ObservedObject var suggestionSettings: SuggestionSettingsModel

/// The dictionary choices only change behavior when a correction can actually be produced and
/// surfaced. The typo gate must be armed (`suppressCompletionsOnTypo`) and at least one
/// correction path active: both "Offer Corrections on Typo" and "Automatically Fix Typos" rank
/// candidates through the enabled SymSpell dictionaries (see `TypoGate`/`bestCorrection`). When
/// none of those is on, ticking a dictionary has no observable effect, so we disable the
/// checkboxes, mirroring how the correction toggles disable themselves when the gate is off.
private var dictionariesAffectCorrections: Bool {
suggestionSettings.suppressCompletionsOnTypo
&& (suggestionSettings.offerTypoCorrections || suggestionSettings.automaticallyFixTypos)
}

var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Spelling Dictionaries")
.font(.system(size: 13, weight: .medium))

Text(
"Choose which bundled dictionaries Cotabby may use for frequency-ranked corrections. "
+ "With several enabled, Cotabby selects one from the surrounding text."
Expand Down Expand Up @@ -51,7 +42,6 @@ struct SpellingDictionaryPicker: View {
.toggleStyle(.checkbox)
}
}
.disabled(!dictionariesAffectCorrections)

Text(
"Indexes load on demand and Cotabby keeps at most two in memory. If no bundled "
Expand Down
Loading