Class: Charming::Focus

Inherits:
Object
  • Object
show all
Defined in:
lib/charming/focus.rb

Overview

Focus manages a stack of focus scopes (rings) for a single controller class. Each scope has a slot ring (a fixed list of named slots) and a current slot within that ring. Multiple scopes can be stacked so the command palette, modals, and layouts can each have their own focus contexts without interfering with one another.

State lives under ‘session[controller_class_name]` so focus persists across controller dispatches within the same session.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(state) ⇒ Focus

Returns a new instance of Focus.



21
22
23
# File 'lib/charming/focus.rb', line 21

def initialize(state)
  @state = state
end

Class Method Details

.for(session, controller_class) ⇒ Object

Returns the Focus object for controller_class under the given session, creating the underlying session hash if absent.



14
15
16
17
18
19
# File 'lib/charming/focus.rb', line 14

def self.for(session, controller_class)
  session[:focus_state] ||= {}
  key = controller_class.name
  session[:focus_state][key] ||= {scopes: []}
  new(session[:focus_state][key])
end

Instance Method Details

#currentObject

Returns the currently focused slot, or nil when no scope is active.



56
57
58
# File 'lib/charming/focus.rb', line 56

def current
  top && top[:current]
end

#cycle(direction = +1) ⇒ Object

Cycles focus by direction (default +1 forward) within the topmost ring. No-op on an empty ring.



73
74
75
76
77
78
# File 'lib/charming/focus.rb', line 73

def cycle(direction = +1)
  return if ring.empty?

  index = ring.index(current) || 0
  top[:current] = ring[(index + direction) % ring.length]
end

#define(slots) ⇒ Object

Defines the primary focus ring for the controller with the given slots. Only effective the first time it is called; subsequent calls are no-ops.



27
28
29
30
31
# File 'lib/charming/focus.rb', line 27

def define(slots)
  return if @state[:scopes].any? { |scope| scope[:origin] == :ring }

  @state[:scopes] << build_scope(slots, :ring)
end

#define_layout(slots) ⇒ Object

Defines a layout scope (inserted after the primary ring and before modal scopes). slots is the list of pane names; the previously-focused layout slot is preserved when it is still part of the new ring.



36
37
38
39
40
41
42
# File 'lib/charming/focus.rb', line 36

def define_layout(slots)
  current = current_layout_slot(slots)
  remove_scope(:layout)
  return if slots.empty?

  @state[:scopes].insert(layout_scope_index, build_scope(slots, :layout, current))
end

#focus(slot) ⇒ Object

Sets the current slot within the topmost scope to slot. No-op when slot is not in the ring.



66
67
68
69
70
# File 'lib/charming/focus.rb', line 66

def focus(slot)
  return unless ring.include?(slot)

  top[:current] = slot
end

#focused?(slot) ⇒ Boolean

True when slot is the current focus slot.

Returns:

  • (Boolean)


81
82
83
# File 'lib/charming/focus.rb', line 81

def focused?(slot)
  current == slot
end

#pop_scopeObject

Pops the topmost focus scope from the stack.



51
52
53
# File 'lib/charming/focus.rb', line 51

def pop_scope
  @state[:scopes].pop
end

#push_scope(slots, origin: :modal) ⇒ Object

Pushes a new focus scope with the given slots onto the stack. Used by modals, palettes, and other overlays. origin is a label for the scope kind.



46
47
48
# File 'lib/charming/focus.rb', line 46

def push_scope(slots, origin: :modal)
  @state[:scopes] << build_scope(slots, origin)
end

#ringObject

Returns the slot ring of the topmost scope (an array of slot names). Empty when no scope.



61
62
63
# File 'lib/charming/focus.rb', line 61

def ring
  top ? top[:ring] : []
end