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
11 changes: 9 additions & 2 deletions focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,13 +582,20 @@ namespace ui {
return !!target && target.scopeId == scopeId && !target.hidden
}

/**
* Drops a scope's retained target when that target is no longer
* eligible. When the scope is active, focus is re-resolved to the
* scope's preferred target so that hiding the focused control does not
* strand focus on something no movement can start from.
*/
private clearRetainedActiveTarget(
scopeId: UiFocusScopeId,
targetId: UiFocusId,
): void {
const scope = this.findScope(scopeId)
if (scope && scope.activeTargetId == targetId)
scope.activeTargetId = undefined
if (!scope || scope.activeTargetId != targetId) return
scope.activeTargetId = undefined
if (this.activeScopeId_ == scopeId) this.activateScope(scope)
}

private activateScope(scope: UiFocusScopeRecord): UiFocusSetResult {
Expand Down
33 changes: 29 additions & 4 deletions screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,24 @@ namespace ui {
return undefined
}

/**
* Renders the root views, then their focus treatments, then the active
* modal. Focus treatments extend past their control, so drawing them in
* a pass of their own keeps a focus label from being covered by a root
* rendered after it. Views that do not separate the two render whole in
* the first pass.
*/
private renderViews(surface: DrawSurface): void {
for (let i = 0; i < this.roots_.length; i++) {
this.roots_[i].view.render(surface, this.assets, this.focus_)
const view = <any>this.roots_[i].view
if (view.renderControls)
view.renderControls(surface, this.assets, this.focus_)
else view.render(surface, this.assets, this.focus_)
Comment on lines 280 to +284
}
for (let i = 0; i < this.roots_.length; i++) {
const view = <any>this.roots_[i].view
if (view.renderFocus)
view.renderFocus(surface, this.assets, this.focus_)
}
if (this.activeModal_)
this.activeModal_.render(surface, this.assets, this.focus_)
Expand Down Expand Up @@ -424,13 +439,23 @@ namespace ui {
return undefined
}

/**
* Focuses the first root that can take focus. Roots whose targets are
* all hidden, and container views that own no scope, are skipped rather
* than swallowing the screen's initial focus.
*/
private focusFirstRoot(): UiFocusSetResult | undefined {
let firstResult: UiFocusSetResult | undefined = undefined
for (let i = 0; i < this.roots_.length; i++) {
const view = this.roots_[i].view
if ((<any>view).registerFocusTargets)
return (<any>view).focusDefault(this.focus_)
if (!(<any>view).registerFocusTargets) continue
const result = <UiFocusSetResult>(
(<any>view).focusDefault(this.focus_)
)
if (result && result.kind == "focused") return result
if (!firstResult) firstResult = result
}
return undefined
return firstResult
}

private defaultHandled<TResult>(result: TResult): boolean | undefined {
Expand Down
64 changes: 63 additions & 1 deletion view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ namespace ui {
export interface UiFocusableView<TResult> extends UiView<TResult> {
/**
* Registers focus targets after layout has arranged this view.
*
* `scopeOptions` lets a parent view register this view's targets under a
* scope the parent owns.
*/
registerFocusTargets(focus: UiFocusState): void
registerFocusTargets(
focus: UiFocusState,
scopeOptions?: UiFocusScopeOptions,
): void

/**
* Registers directional navigation after layout has arranged this view.
Expand All @@ -38,6 +44,62 @@ namespace ui {
focusDefault(focus: UiFocusState): UiFocusSetResult
}

/**
* Focusable view that a parent view can compose into a scope the parent
* owns, so several views navigate as one.
*
* The parent assigns its scope with `setScopeId`, collects each child's
* targets with `navigationRows`, and registers one navigation for the whole
* group. Children keep rendering, measuring, and arranging themselves.
*/
export interface UiComposableFocusView<TResult>
extends UiFocusableView<TResult> {
/**
* Focus scope this view registers its targets under.
*/
scopeId: UiFocusScopeId

/**
* Adopts an owner scope. Target ids are rebuilt from the new scope, so
* this runs before any focus registration.
*/
setScopeId(scopeId: UiFocusScopeId): void

/**
* Rows of navigation targets in movement order, using this view's
* current arranged rectangles and visibility.
*/
navigationRows(): UiFocusNavigationTarget[][]

/**
* Renders this view without its focus treatment.
*
* A focus treatment extends past the control it belongs to, so a parent
* draws every child's controls before any child's focus treatment.
* Otherwise a focus label is covered by whatever is rendered next.
*/
renderControls(
surface: DrawSurface,
assets: UiAssetResolver,
focus?: UiFocusState,
): void

/**
* Renders only this view's focus treatment.
*/
renderFocus(
surface: DrawSurface,
assets: UiAssetResolver,
focus?: UiFocusState,
): void

/**
* Target this view would focus by default, or `undefined` when it has
* no focusable target.
*/
resolvePreferredTargetId(): UiFocusId | undefined
}

/**
* Screen placement for a root view.
*/
Expand Down
Loading