Conversation
Now only widgets that could be focused in some cases get registered. Due to settings like "Keyboard Navigation" it may get run for widgets not actually focusable with the Users Settings, which isn't a problem, except maybe a minor performance hit.
…and made initialFirstResponder respect focusDisabled()
…n faster and be consistent
# Conflicts: # Examples/Bundler.toml # Examples/Package.swift # Sources/AppKitBackend/AppKitBackend.swift # Sources/Gtk/Generated/Entry.swift # Sources/Gtk3/Generated/Entry.swift # Sources/SwiftCrossUI/Backend/AppBackend.swift # Sources/SwiftCrossUI/Environment/EnvironmentValues.swift
todo: fix reverse chain
# Conflicts: # Sources/AppKitBackend/AppKitBackend.swift # Sources/SwiftCrossUI/Environment/EnvironmentValues.swift # Sources/SwiftCrossUI/Scenes/WindowReference.swift
…tainer & prevented crashs on partially supported backends
|
I haven't started reviewing yet, but thanks for the detailed description! I just read through it, and it should help me get oriented a bit quicker. Would you feel comfortable trying to implement some unit tests on top of DummyBackend for these changes? You'd first have to update DummyBackend to support these focus features, which I'm going to request during my review anyway (if you haven't already done so). You can take inspiration from @MainActor focusTestCurrentValue: String
struct FocusTestView: View {
@FocusState var focusState: String?
var body: some View {
...
.onChange(of: focusState) { value in
focusTestCurrentValue = value
}
}
}
// ... in test:
let view = FocusTestView()
// ... get button widget
button.onFocus() // or however you would trigger this
#expect(focusTestCurrentValue == /* ... */)That's all off the top of my head, so it'll definitely need some tweaking |
|
I can do that. Initially I didn't unit test FocusState on DummyBackend, because I felt like the more important cases to test are on actual backends, due to some implementation differences. But it still should be a good base layer, for making sure the SCUI level implementation is correct. |
…ackend, GtkBackend and DummyBackend
This PR introduces comprehensive focus management to SwiftCrossUI, including state-driven focus, focusability control, and control over visual highlight displaying.
Feature Support:
@FocusStateView/focusableView/focusEffectDisabledUnsupported means no-op in this case, with information about lacking support in the console
This took significantly longer than I expected and got quite big, so I will try to guide you a bit haha
This PR adds support for 3 focus related features:
@FocusStateFocusState is a bidirectional property wrapper. If the focus changes through user action it will be reflected in the state and the other way around setting it will focus the corresponding view.
View/focusedworks hand in hand with@FocusState. It needs to be added to a View with aFocusState<T>.Bindingand a unique value (any Hashable) identifying it for a view to set the@FocusStateto the unique value on focus gain and be selected when set programmatically.When multiple views are assigned the same value or multiple leaf views recieve it through the environment, it only works reactive, but only the widget last handled by the layout engine will recieve the focus visibly to the user.
There is an overload if
@FocusStateandView/focusedaccepting a non optional Bool. This would be the preferred choice in cases where you only have one view to focus.UIKitBackendcurrently doesn’t support it.View/focusableView/focusablemodifies the focusability for a subtree. On supported Backends (AppKit & Gtk) setting it toFocusability.disabledexcludes the subtree from recieving focus via keyboard navigation/tab press. Just like SwiftUI it doesn’t prevent focus through mouse click, but the next tab press is going to move focus outside of the subtree again.WinUI sadly doesn’t support it yet, because the equivalents of
NSView/nextValidKeyViewandNSView/previousValidKeyViewall crash. I will create an issue after submitting this PR accordingly.WinUIBackendwill need to implement an approach similar toAppKitBackend, because, unlike Gtk, WinUI doesn’t support hierarchical disabling. More to that later.View/focusEffectDisabled… does exactly what it sounds like, disables the focus effect/hightlight/ring when applied to a widget with parameter
trueCurrently supported by
AppKitBackend,GtkBackendandWinUIBackend.SwiftCrossUI
AppBackendFocusState, only called byViewGraphNodeView/focusableView/focusEffectDisabled, only called byViewGraphNodeEnvironmentValues:View/focusedmodifiers applied on the tree above a viewView/focusEffectDisabledViewLayoutResult:ViewGraphNode/commit:if currentLayout?.shouldSetFocusData == true, registers observers of@FocusDataon the widget and handlesView/focusEffectDisabledViewLayoutResult/shouldSetFocusDataset to true:FocusChainManagerprotocolView/focusable(.disabled)on backends without hierarchical support for disabling views (e.g.AppKitBackendand later hopefullyWinUIBackend)FocusabilityContainerprotocol: gets used byFocusChainManagerto identify a marker viewFocusChainParticipantprotocol: needs to be implemented byAppBackend/Widget, provides information about the visibility of a widget and wether it can be a tab stop@FocusState: basically just a bit more fancy@Stateconstrained toHashable, not accepting an initial value and being able to reset to nil or false (ifHashablehappens to be aBool. The Binding is a specific binding forFocusStatebecause it also needs the reset capabilityFocusData:@FocusState, containing the type, what the value passed toView/focusedis, if the value matches the current state and set&reset closures, used by a backend’sFocusStateManagerFocusability: an enum currently with cases.unmodifiedand.disabled, used byView/focusableAppKitBackend
FocusabilityContainer:Focusability.enabledcaseFocusabilityContainerAppBackendNSWindow/firstResponderand updates@FocusStateaccordinglyNSObservableTextField, as it passes the focus to a child view.[FocusData]contains a match in state to identifying valueNSCustomWindowto separated fileNSCustomWindowto support focus chain manipulation i.e.View/focusableNSMapTableas cacheNSCustomWindow/invalidateCachefunction resetting theNSMapTablesNSWindow/initialFirstRespondergetter to respectView/focusableNSWindow/selectKeyView(preceding:)andNSWindow/selectKeyView(following:)to useFocusChainManagercustom logic, respectingView/focusableNSWindow/recalculateKeyViewLoopFocusStateManagerprotocol requirementsNSContainerView:NSContainerViewinstead ofNSViewGtkCodeGen
EventControllerFocusGtk
Pango/getTextSizeto accept an optional on ellipsize, to restore compatibility withTextEditorWidget:CSSBlockforfocusEffectDisabledsupport. BothCSSBlocks load a concatenation in thecssProviderin didSetisVisible, using the gtk functionisFocusable, using gtks getter and settercanFocus, using gtks getter and setterEventControllerFocusGtkBackend
FocusStateManagerAppKitBackend’sFocusStateManager, but adopted to work with GtkAppBackendFixedon Gtk. Due to very nice hierarchical support no complex logic is needed. Focusability is controlled by settingcanFocuson it. When false, tab originated focus can’t enter anymore. Thank you Gtk, you were awesome to work with this time.CSSBlockonWidgetsize(of text, whenDisplayedIn….)to be compatible withTextEditoragainGtk3Backend
UIKitBackend
WinUIBackend
FocusStateManagerAppKitBackend’sFocusStateManagerbut made to work with WinUIFocusContainer: Canvas, for futureView/focusablesupportAppBackendFocusContaineris used, but doesn’t have an effect yetFrameworkElement/useSystemFocusVisualsCustomWindowwas moved to a separate fileFocusChainManager, but removed it as everything I tried lead to crashes, like mentioned on discordAdditional information
Examples
View/focusable. I would prefer to keep it around to make adoption in other backends easier@FocusState. I would prefer to keep it around to make adoption in other backends easierHow does the
FocusChainManagerlogic work?FocusabilityContainerwithFocusabilityContainer/focusability == .disabledsomewhere above it.I hope the list and explanation helps :)