From 77901274339c13928693c3980a7a83d6416a62e4 Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:55:08 -0700 Subject: [PATCH] feat(settings): make typo Corrections a progressive-disclosure hierarchy The Writing pane showed "Hide Suggestions on Typo", "Offer Corrections on Typo", and "Automatically Fix Typos" as three flat peers, with the latter two merely dimmed when the first was off. The gate relationship was invisible: turning off "Hide Suggestions on Typo" silently killed the other two and the dictionary picker, with nothing to explain why. Reframe the section as the master/dependent idiom already used by EmojiPaneView. The gate now lives alone in a "Typos" section; the "Corrections" actions appear only once it is on; the "Spelling Dictionaries" picker appears only once a correction action that can consume them is on. Revealing each level when it becomes relevant (instead of dimming dependents in place) makes the dependency unmistakable. SpellingDictionaryPicker loses its now-redundant inline title and self-disabling logic since the enclosing Section supplies the header and the parent gates visibility. --- .../UI/Settings/Panes/WritingPaneView.swift | 63 ++++++++++++------- Cotabby/UI/SpellingDictionaryPicker.swift | 20 ++---- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/Cotabby/UI/Settings/Panes/WritingPaneView.swift b/Cotabby/UI/Settings/Panes/WritingPaneView.swift index 7767178b..c7762021 100644 --- a/Cotabby/UI/Settings/Panes/WritingPaneView.swift +++ b/Cotabby/UI/Settings/Panes/WritingPaneView.swift @@ -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) + } + } } Section("Profile") { diff --git a/Cotabby/UI/SpellingDictionaryPicker.swift b/Cotabby/UI/SpellingDictionaryPicker.swift index 491f821f..e9045682 100644 --- a/Cotabby/UI/SpellingDictionaryPicker.swift +++ b/Cotabby/UI/SpellingDictionaryPicker.swift @@ -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." @@ -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 "