User interface toolkit for Go with support for SDL2 and HTML Canvas render targets.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

528 lines
14 KiB

  1. package ui
  2. import (
  3. "errors"
  4. "sync"
  5. "git.kirsle.net/go/render"
  6. "git.kirsle.net/go/render/event"
  7. )
  8. // Event is a named event that the supervisor will send.
  9. type Event int
  10. // Events.
  11. const (
  12. NullEvent Event = iota
  13. MouseOver
  14. MouseOut
  15. MouseDown
  16. MouseUp
  17. Click
  18. KeyDown
  19. KeyUp
  20. KeyPress
  21. // Drag/drop event handlers.
  22. DragStop // if a widget is being dragged and the drag is done
  23. DragMove // mouse movements sent to a widget being dragged.
  24. Drop // a "drop site" widget under the cursor when a drag is done
  25. // Window Manager events.
  26. CloseWindow
  27. MaximizeWindow
  28. MinimizeWindow
  29. CloseModal
  30. // Lifecycle event handlers.
  31. Compute // fired whenever the widget runs Compute
  32. Present // fired whenever the widget runs Present
  33. )
  34. // EventData carries common data to event handlers.
  35. type EventData struct {
  36. // Point is usually the cursor position on click and mouse events.
  37. Point render.Point
  38. // Engine is the render engine on Compute and Present events.
  39. Engine render.Engine
  40. // Supervisor is the reference to the supervisor who sent the event.
  41. Supervisor *Supervisor
  42. }
  43. // Supervisor keeps track of widgets of interest to notify them about
  44. // interaction events such as mouse hovers and clicks in their general
  45. // vicinity.
  46. type Supervisor struct {
  47. lock sync.RWMutex
  48. serial int // ID number of each widget added in order
  49. widgets map[int]WidgetSlot // map of widget ID to WidgetSlot
  50. hovering map[int]interface{} // map of widgets under the cursor
  51. clicked map[int]bool // map of widgets being clicked
  52. dd *DragDrop
  53. // Stack of modal widgets that have event priority.
  54. modals []Widget
  55. // List of window focus history for Window Manager.
  56. winFocus *FocusedWindow
  57. winTop *FocusedWindow // pointer to top-most window
  58. winBottom *FocusedWindow // pointer to bottom-most window
  59. }
  60. // WidgetSlot holds a widget with a unique ID number in a sorted list.
  61. type WidgetSlot struct {
  62. id int
  63. widget Widget
  64. }
  65. // NewSupervisor creates a supervisor.
  66. func NewSupervisor() *Supervisor {
  67. return &Supervisor{
  68. widgets: map[int]WidgetSlot{},
  69. hovering: map[int]interface{}{},
  70. clicked: map[int]bool{},
  71. modals: []Widget{},
  72. dd: NewDragDrop(),
  73. }
  74. }
  75. // DragStart sets the drag state without a widget.
  76. //
  77. // An example where you'd use this is if you want a widget to respond to a
  78. // Drop event (mouse released over a drop-site widget) but the 'thing' being
  79. // dragged is not a ui.Widget, i.e., for custom app specific logic.
  80. func (s *Supervisor) DragStart() {
  81. s.dd.Start()
  82. }
  83. // DragStartWidget sets the drag state to true with a target widget attached.
  84. //
  85. // The widget being dragged is given DragMove events while the drag is
  86. // underway. When the mouse button is released, the widget is given a
  87. // DragStop event and the widget below the cursor is given a Drop event.
  88. func (s *Supervisor) DragStartWidget(w Widget) {
  89. s.dd.SetWidget(w)
  90. s.dd.Start()
  91. }
  92. // DragStop stops the drag state.
  93. func (s *Supervisor) DragStop() {
  94. s.dd.Stop()
  95. }
  96. // IsDragging returns whether the drag state is enabled.
  97. func (s *Supervisor) IsDragging() bool {
  98. return s.dd.IsDragging()
  99. }
  100. // Error messages that may be returned by Supervisor.Loop()
  101. var (
  102. // The caller should STOP forwarding any mouse or keyboard events to any
  103. // other handles for the remainder of this tick.
  104. ErrStopPropagation = errors.New("stop all event propagation")
  105. ErrNoEventHandler = errors.New("no event handler")
  106. )
  107. // Loop to check events and pass them to managed widgets.
  108. //
  109. // Useful errors returned by this may be:
  110. // - ErrStopPropagation
  111. func (s *Supervisor) Loop(ev *event.State) error {
  112. var (
  113. XY = render.Point{
  114. X: ev.CursorX,
  115. Y: ev.CursorY,
  116. }
  117. )
  118. // See if we are hovering over any widgets.
  119. hovering, outside := s.Hovering(XY)
  120. // If we are dragging something around, do not trigger any mouse events
  121. // to other widgets but DO notify any widget we dropped on top of!
  122. if s.dd.IsDragging() {
  123. if !ev.Button1 && !ev.Button3 {
  124. // The mouse has been released. TODO: make mouse button important?
  125. for _, child := range hovering {
  126. child.widget.Event(Drop, EventData{
  127. Point: XY,
  128. })
  129. }
  130. s.DragStop()
  131. } else {
  132. // If we have a target widget being dragged, send it mouse events.
  133. if target := s.dd.Widget(); target != nil {
  134. target.Event(DragMove, EventData{
  135. Point: XY,
  136. })
  137. }
  138. }
  139. return ErrStopPropagation
  140. }
  141. // Check if the top focused window has been closed and auto-focus the next.
  142. if s.winFocus != nil && s.winFocus.window.Hidden() {
  143. next := s.winFocus.next
  144. for next != nil {
  145. if !next.window.Hidden() {
  146. s.FocusWindow(next.window)
  147. break
  148. }
  149. next = next.next
  150. }
  151. }
  152. // Run events in managed windows first, from top to bottom.
  153. // Widgets in unmanaged windows will be handled next.
  154. // err := s.runWindowEvents(XY, ev, hovering, outside)
  155. // Only run if there is no active modal (modals have top priority)
  156. if len(s.modals) == 0 {
  157. handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true)
  158. if err == ErrStopPropagation || handled {
  159. // A widget in the active window has accepted an event. Do not pass
  160. // the event also to lower widgets.
  161. return ErrStopPropagation
  162. }
  163. }
  164. // Run events for the other widgets not in a managed window.
  165. // (Modal event priority is handled in runWidgetEvents)
  166. s.runWidgetEvents(XY, ev, hovering, outside, false)
  167. return nil
  168. }
  169. // Hovering returns all of the widgets managed by Supervisor that are under
  170. // the mouse cursor. Returns the set of widgets below the cursor and the set
  171. // of widgets not below the cursor.
  172. func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSlot) {
  173. var XY = cursor // for shorthand
  174. hovering = []WidgetSlot{}
  175. outside = []WidgetSlot{}
  176. // Check all the widgets under our care.
  177. for child := range s.Widgets() {
  178. var (
  179. w = child.widget
  180. P = AbsolutePosition(w)
  181. S = w.Size()
  182. P2 = render.Point{
  183. X: P.X + S.W,
  184. Y: P.Y + S.H,
  185. }
  186. )
  187. if XY.X >= P.X && XY.X < P2.X && XY.Y >= P.Y && XY.Y < P2.Y {
  188. // Cursor intersects the widget.
  189. hovering = append(hovering, child)
  190. } else {
  191. outside = append(outside, child)
  192. }
  193. }
  194. return hovering, outside
  195. }
  196. // runWindowEvents is a subroutine of Supervisor.Loop().
  197. //
  198. // After determining the widgets below the cursor (hovering) and outside the
  199. // cursor, transmit mouse events to the widgets.
  200. //
  201. // This function has two use cases:
  202. // - In runWindowEvents where we run events for the top-most focused window of
  203. // the window manager.
  204. // - In Supervisor.Loop() for the widgets that are NOT owned by a managed
  205. // window, so that these widgets always get events.
  206. //
  207. // Parameters:
  208. // XY (Point): mouse cursor position as calculated in Loop()
  209. // ev, hovering, outside: values from Loop(), self explanatory.
  210. // behavior: indicates how this method is being used.
  211. //
  212. // behavior options:
  213. // 0: widgets NOT part of a managed window. On this pass, if a widget IS
  214. // a part of a window, it gets no events triggered.
  215. // 1: widgets are part of the active focused window.
  216. func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
  217. hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
  218. // Do we run any events?
  219. var (
  220. stopPropagation bool
  221. ranEvents bool
  222. )
  223. // Do we have active modals? Modal widgets have top event priority given
  224. // only to the top-most modal.
  225. var modal Widget
  226. if len(s.modals) > 0 {
  227. modal = s.modals[len(s.modals)-1]
  228. }
  229. // If we're running this method in "Phase 2" (to widgets NOT in the focused
  230. // window), only send mouse events to widgets if the cursor is NOT inside
  231. // the bounding box of the active focused window. Prevents clicking "thru"
  232. // the window and activating widgets/other windows behind it.
  233. var cursorInsideFocusedWindow bool
  234. if !toFocusedWindow && s.winFocus != nil {
  235. // Get the bounding box of the focused window.
  236. if XY.Inside(AbsoluteRect(s.winFocus.window)) {
  237. cursorInsideFocusedWindow = true
  238. }
  239. }
  240. // Handler for an Event response errors.
  241. handle := func(err error) {
  242. // Did any event handler run?
  243. if err != ErrNoEventHandler {
  244. ranEvents = true
  245. }
  246. // Are we stopping propagation?
  247. if err == ErrStopPropagation {
  248. stopPropagation = true
  249. }
  250. }
  251. for _, child := range hovering {
  252. if stopPropagation {
  253. break
  254. }
  255. // If the cursor is inside the box of the focused window, don't trigger
  256. // active (hovering) mouse events. MouseOut type events, below, can still
  257. // trigger.
  258. // Does not apply when a modal widget is active.
  259. if cursorInsideFocusedWindow && modal == nil {
  260. break
  261. }
  262. var (
  263. id = child.id
  264. w = child.widget
  265. )
  266. if w.Hidden() {
  267. // TODO: somehow the Supervisor wasn't triggering hidden widgets
  268. // anyway, but I don't know why. Adding this check for safety.
  269. continue
  270. }
  271. // If we have a modal active, validate this widget is a child of
  272. // the modal widget.
  273. if modal != nil {
  274. if !HasParent(w, modal) {
  275. continue
  276. }
  277. }
  278. // Check if the widget is part of a Window managed by Supervisor.
  279. isManaged, isFocused := widgetInFocusedWindow(w)
  280. // Are we sending events to it?
  281. if toFocusedWindow {
  282. // Only sending events to widgets owned by the focused window.
  283. if !(isManaged && isFocused) {
  284. continue
  285. }
  286. } else {
  287. // Sending only to widgets NOT managed by a window. This can include
  288. // Window widgets themselves, so lower unfocused windows may be
  289. // brought to foreground.
  290. window, isWindow := w.(*Window)
  291. if isManaged && !isWindow {
  292. continue
  293. }
  294. // It is a window, but can only be the non-focused window.
  295. if isWindow && window.focused {
  296. continue
  297. }
  298. }
  299. // Cursor has intersected the widget.
  300. if _, ok := s.hovering[id]; !ok {
  301. handle(w.Event(MouseOver, EventData{
  302. Point: XY,
  303. }))
  304. s.hovering[id] = nil
  305. }
  306. isClicked, _ := s.clicked[id]
  307. if ev.Button1 {
  308. if !isClicked {
  309. err := w.Event(MouseDown, EventData{
  310. Point: XY,
  311. })
  312. handle(err)
  313. s.clicked[id] = true
  314. }
  315. } else if isClicked {
  316. handle(w.Event(MouseUp, EventData{
  317. Point: XY,
  318. }))
  319. handle(w.Event(Click, EventData{
  320. Point: XY,
  321. }))
  322. delete(s.clicked, id)
  323. }
  324. }
  325. for _, child := range outside {
  326. var (
  327. id = child.id
  328. w = child.widget
  329. )
  330. // If we have a modal active, validate this widget is a child of
  331. // the modal widget.
  332. if modal != nil {
  333. if !HasParent(w, modal) {
  334. continue
  335. }
  336. }
  337. // Cursor is not intersecting the widget.
  338. if _, ok := s.hovering[id]; ok {
  339. handle(w.Event(MouseOut, EventData{
  340. Point: XY,
  341. }))
  342. delete(s.hovering, id)
  343. }
  344. if _, ok := s.clicked[id]; ok {
  345. handle(w.Event(MouseUp, EventData{
  346. Point: XY,
  347. }))
  348. delete(s.clicked, id)
  349. }
  350. }
  351. // If a modal is active and a click was registered outside the modal's
  352. // bounding box, send the CloseModal event.
  353. if modal != nil && !XY.Inside(AbsoluteRect(modal)) {
  354. if ev.Button1 {
  355. modal.Event(CloseModal, EventData{
  356. Supervisor: s,
  357. })
  358. }
  359. }
  360. // If there was a modal, return stopPropagation (so callers that manage
  361. // events externally of go/ui can see that a modal intercepted events)
  362. if modal != nil {
  363. return ranEvents, ErrStopPropagation
  364. }
  365. // If a stopPropagation was called, return it up the stack.
  366. if stopPropagation {
  367. return ranEvents, ErrStopPropagation
  368. }
  369. // If ANY event handler was called, return nil to signal
  370. return ranEvents, nil
  371. }
  372. // Widgets returns a channel of widgets managed by the supervisor in the order
  373. // they were added.
  374. func (s *Supervisor) Widgets() <-chan WidgetSlot {
  375. pipe := make(chan WidgetSlot)
  376. go func() {
  377. for i := 0; i < s.serial; i++ {
  378. if w, ok := s.widgets[i]; ok {
  379. pipe <- w
  380. }
  381. }
  382. close(pipe)
  383. }()
  384. return pipe
  385. }
  386. // Present all widgets managed by the supervisor.
  387. //
  388. // NOTE: only the Window Manager feature uses this method, and this method
  389. // will render the windows from bottom to top with the focused window on top.
  390. // For other widgets, they should be added to a parent Frame that will call
  391. // Present on them each time the parent Presents, or otherwise you need to
  392. // manage the presentation of widgets outside the Supervisor.
  393. func (s *Supervisor) Present(e render.Engine) {
  394. s.lock.RLock()
  395. defer s.lock.RUnlock()
  396. // Render the window manager windows from bottom to top.
  397. s.presentWindows(e)
  398. // Render the modals from bottom to top.
  399. if len(s.modals) > 0 {
  400. for _, modal := range s.modals {
  401. modal.Present(e, modal.Point())
  402. }
  403. }
  404. }
  405. // Add a widget to be supervised. Has no effect if the widget is already
  406. // under the supervisor's care.
  407. func (s *Supervisor) Add(w Widget) {
  408. s.lock.Lock()
  409. // Check it's not already there.
  410. for _, child := range s.widgets {
  411. if child.widget == w {
  412. return
  413. }
  414. }
  415. // Add it.
  416. s.widgets[s.serial] = WidgetSlot{
  417. id: s.serial,
  418. widget: w,
  419. }
  420. s.serial++
  421. s.lock.Unlock()
  422. }
  423. // PushModal sets the widget to be a "modal" for the Supervisor.
  424. //
  425. // Modal widgets have top-most event priority: mouse and click events go ONLY
  426. // to the modal and its descendants. Modals work as a stack: the most recently
  427. // pushed widget is the active modal, and popping the modal will make the
  428. // next most-recent widget be the active modal.
  429. //
  430. // If a Click event registers OUTSIDE the bounds of the modal widget, the
  431. // widget receives a CloseModal event.
  432. //
  433. // Returns the length of the modal stack.
  434. func (s *Supervisor) PushModal(w Widget) int {
  435. s.modals = append(s.modals, w)
  436. return len(s.modals)
  437. }
  438. // PopModal attempts to pop the modal from the stack, but only if the modal
  439. // is at the top of the stack.
  440. //
  441. // A widget may safely attempt to PopModal itself on a CloseModal event to
  442. // close themselves when the user clicks outside their box. If there were a
  443. // newer modal on the stack, this PopModal action would do nothing.
  444. func (s *Supervisor) PopModal(w Widget) bool {
  445. // only can pop if the topmost widget is the one being asked for
  446. if len(s.modals) > 0 && s.modals[len(s.modals)-1] == w {
  447. modal := s.modals[len(s.modals)-1]
  448. modal.Hide()
  449. // pop it off
  450. s.modals = s.modals[:len(s.modals)-1]
  451. return true
  452. }
  453. return false
  454. }
  455. // GetModal returns the modal on the top of the stack, or nil if there is
  456. // no modal on top.
  457. func (s *Supervisor) GetModal() Widget {
  458. if len(s.modals) == 0 {
  459. return nil
  460. }
  461. return s.modals[len(s.modals)-1]
  462. }