├── nw.lua
├── nw.md
├── nw_cocoa.lua
├── nw_demo.lua
├── nw_keyboard.md
├── nw_test.lua
├── nw_winapi.lua
└── nw_xlib.lua
/nw.lua:
--------------------------------------------------------------------------------
1 |
2 | --Cross-platform windows for Lua.
3 | --Written by Cosmin Apreutesei. Public domain.
4 |
5 | local ffi = require'ffi'
6 | local glue = require'glue'
7 | local box2d = require'box2d'
8 | local events = require'events'
9 | local time = require'time'
10 |
11 | local assert = glue.assert --assert with string.format
12 | local indexof = glue.indexof
13 |
14 | local nw = {}
15 |
16 | local backends = {
17 | Windows = 'nw_winapi',
18 | OSX = 'nw_cocoa',
19 | Linux = 'nw_xlib',
20 | }
21 | local bkname = assert(backends[ffi.os], 'unsupported OS %s', ffi.os)
22 | nw.backend = require(bkname)
23 | nw.backend.frontend = nw
24 |
25 | --helpers --------------------------------------------------------------------
26 |
27 | local function optarg(opt, true_arg, false_arg, nil_arg)
28 | opt = glue.index(opt)
29 | return function(arg)
30 | if arg == true then
31 | return true_arg
32 | elseif arg == false then
33 | return false_arg
34 | elseif arg == nil then
35 | return nil_arg
36 | elseif opt[arg] then
37 | return arg
38 | else
39 | error('invalid argument', 2)
40 | end
41 | end
42 | end
43 |
44 | --oo -------------------------------------------------------------------------
45 |
46 | local object = {}
47 |
48 | function object:dead()
49 | return self._dead or false
50 | end
51 |
52 | function object:_check()
53 | assert(not self._dead, 'dead object')
54 | end
55 |
56 | --create a get / set method `m() -> v` / `m(v)` implemented via calls to
57 | --separate getter and setter methods in the backend.
58 | function object:_property(name)
59 | local getter = 'get_'..name
60 | local setter = 'set_'..name
61 | self[name] = function(self, val)
62 | self:_check()
63 | if val == nil then
64 | return self.backend[getter](self.backend)
65 | else
66 | self.backend[setter](self.backend, val)
67 | end
68 | end
69 | end
70 |
71 | --events ---------------------------------------------------------------------
72 |
73 | glue.update(object, events)
74 |
75 | local fire = object.fire
76 | function object:fire(...)
77 | if self._dead then return end
78 | if self._events_disabled then return end
79 | return fire(self, ...)
80 | end
81 |
82 | --enable or disable events. returns the old state.
83 | function object:events(enabled)
84 | if enabled == nil then
85 | return not self._events_disabled
86 | end
87 | local old = not self._events_disabled
88 | self._events_disabled = not enabled
89 | return old
90 | end
91 |
92 | --app object -----------------------------------------------------------------
93 |
94 | local app = glue.update({}, object)
95 |
96 | --return the singleton app object.
97 | --load a default backend on the first call if no backend was set by the user.
98 | function nw:app()
99 | if not self._app then
100 | self._app = app:_init(self, self.backend.app)
101 | end
102 | return self._app
103 | end
104 |
105 | function app:_init(nw, backend_class)
106 | self.nw = nw
107 | self._running = false
108 | self._windows = {} --{window1, ...}
109 | self._notifyicons = {} --{icon = true}
110 | self._autoquit = true --quit after the last visible window closes
111 | self._ignore_numlock = false --ignore the state of the numlock key on keyboard events
112 | self.backend = backend_class:init(self)
113 | self._state = self:_get_state()
114 | return self
115 | end
116 |
117 | --version checks -------------------------------------------------------------
118 |
119 | --check if v2 >= v1, where v1 and v2 have the form 'maj.min.etc...'.
120 | local function check_version(v1, v2)
121 | local v1 = v1:lower()
122 | local v2 = v2:lower()
123 | local ret
124 | while v1 ~= '' do --while there's another part of ver1 to check...
125 | if v2 == '' then --there's no part of ver2 to check against.
126 | return false
127 | end
128 | local n1, n2
129 | n1, v1 = v1:match'^(%d*)%.?(.*)' --eg. '3.0' -> '3', '0'
130 | n2, v2 = v2:match'^(%d*)%.?(.*)'
131 | assert(n1 ~= '', 'invalid syntax') --ver1 part is a dot.
132 | assert(n2 ~= '', 'invalid syntax') --ver2 part is a dot.
133 | if ret == nil then --haven't decided yet
134 | if n1 ~= '' then --above checks imply n2 ~= '' also.
135 | local n1 = tonumber(n1)
136 | local n2 = tonumber(n2)
137 | if n1 ~= n2 then --version parts are different, decide now.
138 | ret = n2 > n1
139 | end
140 | end
141 | end
142 | end
143 | if ret ~= nil then --a comparison has been made.
144 | return ret
145 | end
146 | return true --no more parts of v1 to check.
147 | end
148 |
149 | local qcache = {} --{query = true|false}
150 | function app:ver(q)
151 | if qcache[q] == nil then
152 | local what, qver = q:match'^([^%s]+)%s*(.*)$'
153 | assert(what, 'invalid query')
154 | local ver = self.backend:ver(what:lower())
155 | qcache[q] = ver and (qver == '' and ver or check_version(qver, ver)) or false
156 | end
157 | return qcache[q]
158 | end
159 |
160 | --message loop and timers ----------------------------------------------------
161 |
162 | local password = {} --distinguish yielding via app:sleep() from other yielding
163 |
164 | --sleep function that can be used inside the function passed to app:run().
165 | --unlike time.sleep(), it allows processing of events while waiting.
166 | function app:sleep(seconds) --no arg, true or false means sleep forever.
167 | coroutine.yield(password, seconds or true)
168 | end
169 |
170 | --start the main loop and/or run a function asynchronously.
171 | function app:run(func)
172 |
173 | --schedule to run a function asynchronously.
174 | if func then
175 | local proc = coroutine.wrap(function()
176 | local ok, err = xpcall(func, debug.traceback)
177 | if not ok then
178 | error(err, 2)
179 | end
180 | coroutine.yield(password) --proc finished
181 | end)
182 | local was_running = self._running
183 | local function step()
184 | local pwd, sleep_time = proc()
185 | assert(pwd == password, 'yield in async proc')
186 | if not sleep_time then --proc finished
187 | --if app was not running when we started, stop it back
188 | if not was_running then
189 | self:stop()
190 | end
191 | return
192 | end
193 | if sleep_time == true then return end --sleep forever
194 | self:runafter(sleep_time, step)
195 | end
196 | self:runafter(0, step)
197 | end
198 |
199 | if self._running then return end --ignore while running
200 | self._running = true --run() barrier
201 | self.backend:run()
202 | self._running = false
203 | self._stopping = false --stop() barrier
204 | end
205 |
206 | function app:poll(timeout)
207 | return self.backend:poll(timeout)
208 | end
209 |
210 | function app:running()
211 | return self._running
212 | end
213 |
214 | function app:stop()
215 | --if not self._running then return end --ignore while not running
216 | if self._stopping then return end --ignore repeated attempts
217 | self._stopping = true
218 | self.backend:stop()
219 | end
220 |
221 | function app:runevery(seconds, func)
222 | seconds = math.max(0, seconds)
223 | self.backend:runevery(seconds, func)
224 | end
225 |
226 | function app:runafter(seconds, func)
227 | self:runevery(seconds, function()
228 | func()
229 | return false
230 | end)
231 | end
232 |
233 | app._maxfps = 60
234 |
235 | function app:maxfps(fps)
236 | if fps == nil then
237 | return self._maxfps or false
238 | else
239 | self._maxfps = fps
240 | end
241 | end
242 |
243 | --quitting -------------------------------------------------------------------
244 |
245 | function app:autoquit(autoquit)
246 | if autoquit == nil then
247 | return self._autoquit
248 | else
249 | self._autoquit = autoquit
250 | end
251 | end
252 |
253 | --ask the app and all windows if they can quit. need unanimous agreement to quit.
254 | function app:_canquit()
255 | self._quitting = true --quit() barrier
256 |
257 | local allow = self:fire'quitting' ~= false
258 |
259 | for _,win in ipairs(self:windows()) do
260 | if not win:dead() and not win:parent() then
261 | allow = win:_canclose('quit', nil) and allow
262 | end
263 | end
264 |
265 | self._quitting = nil
266 | return allow
267 | end
268 |
269 | function app:_forcequit()
270 | self._quitting = true --quit() barrier
271 |
272 | local t = self:windows()
273 | for i = #t, 1, -1 do
274 | local win = t[i]
275 | if not win:dead() and not win:parent() then
276 | win:free'force'
277 | end
278 | end
279 |
280 | --free notify icons otherwise they hang around (both in XP and in OSX).
281 | self:_free_notifyicons()
282 | self:_free_dockicon()
283 | self:stop()
284 |
285 | self._quitting = nil
286 | end
287 |
288 | function app:quit()
289 | if self._quitting then return end --ignore if already quitting
290 | if not self._running then return end --ignore if not running
291 | if self:_canquit() then
292 | self:_forcequit()
293 | end
294 | end
295 |
296 | function app:_backend_quitting()
297 | self:quit()
298 | end
299 |
300 | --window list ----------------------------------------------------------------
301 |
302 | --get existing windows in creation order
303 | function app:windows(arg, filter)
304 | if arg == '#' then
305 | if filter then
306 | local n = 0
307 | for _,win in ipairs(self._windows) do
308 | n = n + (filter(win) ~= false and 1 or 0)
309 | end
310 | return n
311 | else
312 | return #self._windows
313 | end
314 | elseif type(arg) == 'function' then
315 | local t = {}
316 | for _,win in ipairs(self._windows) do
317 | if filter(win) ~= false then
318 | t[#t+1] = win
319 | end
320 | end
321 | return t
322 | else
323 | return glue.extend({}, self._windows) --take a snapshot
324 | end
325 | end
326 |
327 | function app:_window_created(win)
328 | table.insert(self._windows, win)
329 | self:fire('window_created', win)
330 | end
331 |
332 | function app:_window_closed(win)
333 | self:fire('window_closed', win)
334 | table.remove(self._windows, indexof(win, self._windows))
335 | end
336 |
337 | --windows --------------------------------------------------------------------
338 |
339 | local window = glue.update({}, object)
340 |
341 | local defaults = {
342 | --state
343 | visible = true,
344 | minimized = false,
345 | maximized = false,
346 | enabled = true,
347 | --positioning
348 | min_cw = 1,
349 | min_ch = 1,
350 | --frame
351 | title = '',
352 | transparent = false,
353 | corner_radius = 0,
354 | background_color = false,
355 | --behavior
356 | topmost = false,
357 | minimizable = true,
358 | maximizable = true,
359 | closeable = true,
360 | resizeable = true,
361 | fullscreenable = true,
362 | activable = true,
363 | autoquit = false, --quit the app on closing
364 | hideonclose = true, --only hide on close without freeing the window
365 | edgesnapping = 'screen',
366 | sticky = false, --only for child windows
367 | }
368 |
369 | --default overrides for parented windows
370 | local defaults_child = {
371 | minimizable = false,
372 | maximizable = false,
373 | fullscreenable = false,
374 | edgesnapping = 'parent siblings screen',
375 | sticky = true,
376 | }
377 |
378 | local opengl_defaults = {
379 | version = '1.0',
380 | vsync = true,
381 | fsaa = false,
382 | }
383 |
384 | local function opengl_options(t)
385 | if not t then return end
386 | local glopt = glue.update({}, opengl_defaults)
387 | if t ~= true then
388 | glue.update(glopt, t)
389 | end
390 | return glopt
391 | end
392 |
393 | local frame_types = glue.index{'normal', 'none', 'toolbox'}
394 | local function checkframe(frame)
395 | frame =
396 | frame == true and 'normal' or
397 | frame == false and 'none' or
398 | frame or 'normal'
399 | assert(frame_types[frame], 'invalid frame type')
400 | return frame
401 | end
402 |
403 | function app:window(...)
404 | local t
405 | if type((...)) ~= 'table' then
406 | local cw, ch, title, visible = ...
407 | t = {cw = cw, ch = ch, title = title, visible = visible}
408 | else
409 | t = ...
410 | end
411 | return window:_new(self, self.backend.window, t)
412 | end
413 |
414 | function window:_new(app, backend_class, useropt)
415 |
416 | --check/normalize args.
417 | local opt = glue.update({},
418 | defaults,
419 | useropt.parent and defaults_child or nil,
420 | useropt)
421 | opt.frame = checkframe(opt.frame)
422 | opt.opengl = opengl_options(useropt.opengl)
423 |
424 | --non-activable windows must be frameless (Windows limitation)
425 | if not opt.activable then
426 | assert(opt.frame == 'none', 'windows with a title bar cannot be non-activable')
427 | end
428 |
429 | if opt.parent then
430 | --prevent creating child windows in parent's closed() event or after.
431 | assert(not opt.parent._closed, 'parent is closed')
432 | --child windows can't be minimizable because they don't show in taskbar.
433 | assert(not opt.minimizable, 'child windows cannot be minimizable')
434 | assert(not opt.minimized, 'child windows cannot be minimized')
435 | --child windows can't be maximizable or fullscreenable (X11 limitation).
436 | assert(not opt.maximizable, 'child windows cannot be maximizable')
437 | assert(not opt.fullscreenable, 'child windows cannot be fullscreenable')
438 | end
439 |
440 | if opt.sticky then
441 | assert(opt.parent, 'sticky windows must have a parent')
442 | end
443 |
444 | --unparented toolboxes don't make sense because they don't show in taskbar
445 | --so they can't be activated when they are completely behind other windows.
446 | --they can't be (minimiz|maximiz|fullscreen)able either (Windows/X11 limitation).
447 | if opt.frame == 'toolbox' then
448 | assert(opt.parent, 'toolbox windows must have a parent')
449 | end
450 |
451 | --transparent windows must be frameless (Windows limitation)
452 | if opt.transparent then
453 | assert(opt.frame == 'none', 'transparent windows must be frameless')
454 | end
455 |
456 | if not opt.resizeable then
457 | if useropt.maximizable == nil then opt.maximizable = false end
458 | if useropt.fullscreenable == nil then opt.fullscreenable = false end
459 | assert(not opt.maximizable, 'a maximizable window cannot be non-resizeable')
460 | assert(not opt.fullscreenable, 'a fullscreenable cannot be non-resizeable')
461 | end
462 |
463 | --maxsize constraints result in undefined behavior in maximized and fullscreen state.
464 | --they work except in Unity which doesn't respect them when maximizing.
465 | --also Windows doesn't center the window on screen in fullscreen mode.
466 | if opt.max_cw or opt.max_ch then
467 | assert(not opt.maximizable, 'a maximizable window cannot have a maximum size')
468 | assert(not opt.fullscreenable, 'a fullscreenable window cannot have a maximum size')
469 | end
470 |
471 | --if missing some frame coords but given some client coords, convert client
472 | --coords to frame coords, and replace missing frame coords with the result.
473 | if not (opt.x and opt.y and opt.w and opt.h) and (opt.cx or opt.cy or opt.cw or opt.ch) then
474 | local x1, y1, w1, h1 = app:client_to_frame(
475 | opt.frame,
476 | opt.menu and true or false,
477 | opt.resizeable and true or false,
478 | opt.cx or 0,
479 | opt.cy or 0,
480 | opt.cw or 0,
481 | opt.ch or 0)
482 | opt.x = opt.x or (opt.cx and x1)
483 | opt.y = opt.y or (opt.cy and y1)
484 | opt.w = opt.w or (opt.cw and w1)
485 | opt.h = opt.h or (opt.ch and h1)
486 | end
487 |
488 | --width and height must be given, either of the client area or of the frame.
489 | assert(opt.w, 'w or cw expected')
490 | assert(opt.h, 'h or ch expected')
491 |
492 | --either cascading or fixating the position, there's no mix.
493 | assert((not opt.x) == (not opt.y),
494 | 'both x (or cx) and y (or cy) or none expected')
495 |
496 | if opt.x == 'center-main' or opt.x == 'center-active' then
497 | local disp = opt.x == 'center-active'
498 | and app:active_display() or app:main_display()
499 | opt.x = disp.cx + (disp.cw - opt.w) / 2
500 | end
501 |
502 | if opt.y == 'center-main' or opt.y == 'center-active' then
503 | local disp = opt.y == 'center-active'
504 | and app:active_display() or app:main_display()
505 | opt.y = disp.cy + (disp.ch - opt.h) / 2
506 | end
507 |
508 | --avoid zero client sizes (X limitation)
509 | opt.min_cw = math.max(opt.min_cw, 1)
510 | opt.min_ch = math.max(opt.min_ch, 1)
511 |
512 | --avoid negative corner radius
513 | opt.corner_radius = math.max(opt.corner_radius, 0)
514 |
515 | self = glue.update({app = app}, self)
516 |
517 | --stored properties
518 | self._parent = opt.parent
519 | self._frame = opt.frame
520 | self._transparent = opt.transparent
521 | self._corner_radius = opt.corner_radius
522 | self._background_color = opt.background_color
523 | self._minimizable = opt.minimizable
524 | self._maximizable = opt.maximizable
525 | self._closeable = opt.closeable
526 | self._resizeable = opt.resizeable
527 | self._fullscreenable = opt.fullscreenable
528 | self._activable = opt.activable
529 | self._autoquit = opt.autoquit
530 | self._hideonclose = opt.hideonclose
531 | self._sticky = opt.sticky
532 | self._opengl = opt.opengl
533 | self:edgesnapping(opt.edgesnapping)
534 |
535 | --internal state
536 | self._mouse = {inside = false}
537 | self._down = {}
538 | self._views = {}
539 | self._cursor_visible = true
540 | self._cursor = 'arrow'
541 |
542 | self.backend = backend_class:new(app.backend, self, opt)
543 |
544 | --cached window state
545 | self._state = self:_get_state()
546 | self._client_rect = {self:client_rect()}
547 | self._frame_rect = {self:frame_rect()}
548 |
549 | self:_init_manual_resize()
550 |
551 | app:_window_created(self)
552 |
553 | self:invalidate()
554 |
555 | --windows are created hidden to allow proper setup before events start.
556 | if opt.visible then
557 | self:show()
558 | end
559 |
560 | if opt.tooltip then
561 | self:tooltip(tooltip)
562 | end
563 |
564 | return self
565 | end
566 |
567 | --closing --------------------------------------------------------------------
568 |
569 | function window:_canclose(reason, closing_window)
570 | if reason == true then return true end --force
571 | if self._closing then return false end --reject while closing
572 | self._closing = true --_canclose() barrier
573 | local allow = self:fire('closing', reason, closing_window) ~= false
574 | --children must agree too
575 | for i,win in ipairs(self:children()) do
576 | allow = win:_canclose(reason, closing_window) and allow
577 | end
578 | self._closing = nil
579 | return allow
580 | end
581 |
582 | function window:close(reason)
583 | if self:hideonclose() and not self:visible() then
584 | return
585 | end
586 | if self:_backend_closing(reason) then
587 | if not self._quitapp and self:hideonclose() then
588 | self:hide()
589 | else
590 | self.backend:forceclose()
591 | end
592 | end
593 | end
594 |
595 | function window:free(dontask)
596 | if self:_backend_closing(dontask == true or 'free', true) then
597 | self.backend:forceclose()
598 | end
599 | end
600 |
601 | local function is_alive_root_and_visible(win)
602 | return not win:dead() and not win:parent() and win:visible()
603 | end
604 | function window:_backend_closing(reason, donthide)
605 | if self._closed then return false end --reject if closed
606 |
607 | if not self:_canclose(reason or 'close', self) then
608 | return false
609 | end
610 |
611 | if self:autoquit() or (
612 | app:autoquit()
613 | and not self:parent() --closing a root window
614 | and app:windows('#', is_alive_root_and_visible) == 1 --the only one
615 | ) then
616 | self._quitapp = app:_canquit() or nil
617 | end
618 |
619 | if not donthide and not self._quitapp and self:hideonclose() then
620 | self:hide()
621 | return false
622 | end
623 |
624 | return true
625 | end
626 |
627 | function window:_backend_closed()
628 | if self._closed then return end --ignore if closed
629 | self._closed = true --_backend_closing() and _backend_closed() barrier
630 |
631 | self:fire'closed'
632 | app:_window_closed(self)
633 |
634 | self:_free_views()
635 | self._dead = true
636 |
637 | if self._quitapp then
638 | app:_forcequit()
639 | self._quitapp = nil
640 | end
641 | end
642 |
643 | --activation -----------------------------------------------------------------
644 |
645 | local modes = glue.index{'alert', 'force', 'info'}
646 | function app:activate(mode)
647 | mode = mode or 'alert'
648 | assert(modes[mode], 'invalid mode')
649 | self.backend:activate(mode)
650 | end
651 |
652 | function app:active_window()
653 | return self.backend:active_window()
654 | end
655 |
656 | function app:active()
657 | return self.backend:active()
658 | end
659 |
660 | function window:activate()
661 | self:_check()
662 | if not self:visible() then return end
663 | self.backend:activate()
664 | end
665 |
666 | function window:active()
667 | self:_check()
668 | if not self:visible() then return false end --false if hidden
669 | return self.backend:active()
670 | end
671 |
672 | --single app instance --------------------------------------------------------
673 |
674 | function app:already_running()
675 | return self.backend:already_running()
676 | end
677 |
678 | function app:wakeup_other_instances()
679 | self.backend:wakeup_other_instances()
680 | end
681 |
682 | function app:_backend_wakeup()
683 | self:fire'wakeup'
684 | end
685 |
686 | function app:check_single_instance()
687 | if self:already_running() then
688 | self:wakeup_other_instances()
689 | os.exit(0)
690 | end
691 | self:on('wakeup', function(self)
692 | self:activate()
693 | end)
694 | end
695 |
696 | --state/app visibility (OSX only) --------------------------------------------
697 |
698 | function app:visible(visible)
699 | if visible == nil then
700 | return self.backend:visible()
701 | elseif visible then
702 | self:unhide()
703 | else
704 | self:hide()
705 | end
706 | end
707 |
708 | function app:unhide()
709 | self.backend:unhide()
710 | end
711 |
712 | function app:hide()
713 | self.backend:hide()
714 | end
715 |
716 | --state/visibility -----------------------------------------------------------
717 |
718 | function window:visible(visible)
719 | self:_check()
720 | if visible == nil then
721 | return self.backend:visible()
722 | elseif visible then
723 | self:show()
724 | else
725 | self:hide()
726 | end
727 | end
728 |
729 | function window:show()
730 | self:_check()
731 | self.backend:show()
732 | end
733 |
734 | function window:hide()
735 | self:_check()
736 | if self:fullscreen() then return end
737 | self.backend:hide()
738 | end
739 |
740 | function window:showmodal()
741 | assert(self:activable(), 'window cannot be shown modal: non-activable')
742 | assert(self:parent(), 'without cannot be shown modal: no parent')
743 | self:once('hidden', function(self)
744 | self:parent():enabled(true)
745 | end)
746 | self:parent():enabled(false)
747 | self:show()
748 | end
749 |
750 | --state/minimizing -----------------------------------------------------------
751 |
752 | function window:isminimized()
753 | self:_check()
754 | if self:parent() then
755 | return false -- child windows cannot be minimized
756 | end
757 | return self.backend:minimized()
758 | end
759 |
760 | function window:minimize()
761 | self:_check()
762 | self.backend:minimize()
763 | end
764 |
765 | --state/maximizing -----------------------------------------------------------
766 |
767 | function window:ismaximized()
768 | self:_check()
769 | return self.backend:maximized()
770 | end
771 |
772 | function window:maximize()
773 | self:_check()
774 | self.backend:maximize()
775 | end
776 |
777 | --state/restoring ------------------------------------------------------------
778 |
779 | function window:restore()
780 | self:_check()
781 | if self:visible() and self:fullscreen() then
782 | self:fullscreen(false)
783 | else
784 | self.backend:restore()
785 | end
786 | end
787 |
788 | function window:shownormal()
789 | self:_check()
790 | self.backend:shownormal()
791 | end
792 |
793 | --state/fullscreen -----------------------------------------------------------
794 |
795 | function window:fullscreen(fullscreen)
796 | self:_check()
797 | if fullscreen == nil then
798 | return self.backend:fullscreen()
799 | elseif fullscreen then
800 | self.backend:enter_fullscreen()
801 | else
802 | self.backend:exit_fullscreen()
803 | end
804 | end
805 |
806 | --state/state string ---------------------------------------------------------
807 |
808 | function window:_get_state()
809 | local t = {}
810 | table.insert(t, self:visible() and 'visible' or nil)
811 | table.insert(t, self:isminimized() and 'minimized' or nil)
812 | table.insert(t, self:ismaximized() and 'maximized' or nil)
813 | table.insert(t, self:fullscreen() and 'fullscreen' or nil)
814 | table.insert(t, self:active() and 'active' or nil)
815 | return table.concat(t, ' ')
816 | end
817 |
818 | function app:_get_state()
819 | local t = {}
820 | table.insert(t, self:visible() and 'visible' or nil)
821 | table.insert(t, self:active() and 'active' or nil)
822 | return table.concat(t, ' ')
823 | end
824 |
825 | --state/change event ---------------------------------------------------------
826 |
827 | local function diff(s, old, new)
828 | local olds = old:find(s, 1, true) and 1 or 0
829 | local news = new:find(s, 1, true) and 1 or 0
830 | return news - olds
831 | end
832 |
833 | local function trigger(self, diff, event_up, event_down)
834 | if diff > 0 then
835 | self:fire(event_up)
836 | elseif diff < 0 then
837 | self:fire(event_down)
838 | end
839 | end
840 |
841 | function window:_rect_changed(old_rect, new_rect, changed_event, moved_event, resized_event)
842 | if self:dead() then return end
843 | local x0, y0, w0, h0 = unpack(old_rect)
844 | local x1, y1, w1, h1 = unpack(new_rect)
845 | local moved = x1 ~= x0 or y1 ~= y0
846 | local resized = w1 ~= w0 or h1 ~= h0
847 | if moved or resized then
848 | self:fire(changed_event, x1, y1, w1, h1, x0, y0, w0, h0)
849 | end
850 | if moved then
851 | self:fire(moved_event, x1, y1, x0, y0)
852 | end
853 | if resized then
854 | self:fire(resized_event, w1, h1, w0, h0)
855 | end
856 | return new_rect
857 | end
858 |
859 | function window:_backend_changed()
860 | if self._events_disabled then return end
861 | --check if the state has really changed and generate synthetic events
862 | --for each state flag that has actually changed.
863 | local old = self._state
864 | local new = self:_get_state()
865 | self._state = new
866 | if new ~= old then
867 | self:fire('changed', old, new)
868 | trigger(self, diff('visible', old, new), 'shown', 'hidden')
869 | trigger(self, diff('minimized', old, new), 'minimized', 'unminimized')
870 | trigger(self, diff('maximized', old, new), 'maximized', 'unmaximized')
871 | trigger(self, diff('fullscreen', old, new), 'entered_fullscreen', 'exited_fullscreen')
872 | trigger(self, diff('active', old, new), 'activated', 'deactivated')
873 | end
874 | self._client_rect = self:_rect_changed(self._client_rect, {self:client_rect()},
875 | 'client_rect_changed', 'client_moved', 'client_resized')
876 | self._frame_rect = self:_rect_changed(self._frame_rect, {self:frame_rect()},
877 | 'frame_rect_changed', 'frame_moved', 'frame_resized')
878 | end
879 |
880 | function app:_backend_changed()
881 | local old = self._state
882 | local new = self:_get_state()
883 | self._state = new
884 | if new ~= old then
885 | self:fire('changed', old, new)
886 | trigger(self, diff('hidden', old, new), 'hidden', 'unhidden')
887 | trigger(self, diff('active', old, new), 'activated', 'deactivated')
888 | end
889 | end
890 |
891 | --state/enabled --------------------------------------------------------------
892 |
893 | window:_property'enabled'
894 |
895 | --positioning/helpers --------------------------------------------------------
896 |
897 | local function override_point(x, y, x1, y1)
898 | return x1 or x, y1 or y
899 | end
900 |
901 | local function override_rect(x, y, w, h, x1, y1, w1, h1)
902 | return x1 or x, y1 or y, w1 or w, h1 or h
903 | end
904 |
905 | local function frame_rect(x, y, w, h, w1, h1, w2, h2)
906 | return x - w1, y - h1, w + w1 + w2, h + h1 + h2
907 | end
908 |
909 | local function unframe_rect(x, y, w, h, w1, h1, w2, h2)
910 | local x, y, w, h = frame_rect(x, y, w, h, -w1, -h1, -w2, -h2)
911 | w = math.max(1, w) --avoid zero client sizes
912 | h = math.max(1, h)
913 | return x, y, w, h
914 | end
915 |
916 | --positioning/frame extents --------------------------------------------------
917 |
918 | function app:frame_extents(frame, has_menu, resizeable)
919 | frame = checkframe(frame)
920 | if frame == 'none' then
921 | return 0, 0, 0, 0
922 | end
923 | return self.backend:frame_extents(frame, has_menu, resizeable)
924 | end
925 |
926 | function app:client_to_frame(frame, has_menu, resizeable, x, y, w, h)
927 | return frame_rect(x, y, w, h, self:frame_extents(frame, has_menu, resizeable))
928 | end
929 |
930 | function app:frame_to_client(frame, has_menu, resizeable, x, y, w, h)
931 | return unframe_rect(x, y, w, h, self:frame_extents(frame, has_menu, resizeable))
932 | end
933 |
934 | --positioning/client rect ----------------------------------------------------
935 |
936 | function window:_can_get_rect()
937 | return not self:isminimized()
938 | end
939 |
940 | function window:_can_set_rect()
941 | return not (self:isminimized() or self:ismaximized() or self:fullscreen())
942 | end
943 |
944 | function window:_get_client_size()
945 | if not self:_can_get_rect() then return end
946 | return self.backend:get_client_size()
947 | end
948 |
949 | function window:_get_client_pos()
950 | if not self:_can_get_rect() then return end
951 | return self.backend:get_client_pos()
952 | end
953 |
954 | --convert point in client space to screen space.
955 | function window:to_screen(x, y)
956 | local cx, cy = self:_get_client_pos()
957 | if not cx then return end
958 | return cx+x, cy+y
959 | end
960 |
961 | --convert point in screen space to client space.
962 | function window:to_client(x, y)
963 | local cx, cy = self:_get_client_pos()
964 | if not cx then return end
965 | return x-cx, y-cy
966 | end
967 |
968 | function window:client_size(cw, ch) --sets or returns cw, ch
969 | if cw or ch then
970 | if not cw or not ch then
971 | local cw0, ch0 = self:client_size()
972 | cw = cw or cw0
973 | ch = ch or ch0
974 | end
975 | self:client_rect(nil, nil, cw, ch)
976 | else
977 | return self:_get_client_size()
978 | end
979 | end
980 |
981 | function window:client_rect(x1, y1, w1, h1)
982 | if x1 or y1 or w1 or h1 then
983 | if not self:_can_set_rect() then return end
984 | local cx, cy, cw, ch = self:client_rect()
985 | local ccw, cch = cw, ch
986 | local cx, cy, cw, ch = override_rect(cx, cy, cw, ch, x1, y1, w1, h1)
987 | local x, y, w, h = self:frame_rect()
988 | local dx, dy = self:to_client(x, y)
989 | local dw, dh = w - ccw, h - cch
990 | self.backend:set_frame_rect(cx + dx, cy + dy, cw + dw, ch + dh)
991 | else
992 | local x, y = self:_get_client_pos()
993 | if not x then return end
994 | return x, y, self:_get_client_size()
995 | end
996 | end
997 |
998 | --positioning/frame rect -----------------------------------------------------
999 |
1000 | function window:frame_rect(x, y, w, h) --returns x, y, w, h
1001 | if x or y or w or h then
1002 | if not self:_can_set_rect() then return end
1003 | if not (x and y and w and h) then
1004 | local x0, y0, w0, h0 = self:frame_rect()
1005 | x, y, w, h = override_rect(x0, y0, w0, h0, x, y, w, h)
1006 | end
1007 | self.backend:set_frame_rect(x, y, w, h)
1008 | else
1009 | if not self:_can_get_rect() then return end
1010 | return self.backend:get_frame_rect()
1011 | end
1012 | end
1013 |
1014 | function window:normal_frame_rect()
1015 | self:_check()
1016 | return self.backend:get_normal_frame_rect()
1017 | end
1018 |
1019 | --positioning/constraints ----------------------------------------------------
1020 |
1021 | function window:minsize(w, h) --pass false to disable
1022 | if w == nil and h == nil then
1023 | return self.backend:get_minsize()
1024 | else
1025 | --clamp to maxsize to avoid undefined behavior in the backend.
1026 | local maxw, maxh = self:maxsize()
1027 | if w and maxw then w = math.min(w, maxw) end
1028 | if h and maxh then h = math.min(h, maxh) end
1029 | --clamp to 1 to avoid zero client sizes.
1030 | w = math.max(1, w or 0)
1031 | h = math.max(1, h or 0)
1032 | self.backend:set_minsize(w, h)
1033 | end
1034 | end
1035 |
1036 | function window:maxsize(w, h) --pass false to disable
1037 | if w == nil and h == nil then
1038 | return self.backend:get_maxsize()
1039 | else
1040 | assert(not self:maximizable(), 'a maximizable window cannot have maxsize')
1041 | assert(not self:fullscreenable(), 'a fullscreenable window cannot have maxsize')
1042 |
1043 | --clamp to minsize to avoid undefined behavior in the backend
1044 | local minw, minh = self:minsize()
1045 | if w and minw then w = math.max(w, minw) end
1046 | if h and minh then h = math.max(h, minh) end
1047 | self.backend:set_maxsize(w or nil, h or nil)
1048 | end
1049 | end
1050 |
1051 | --positioning/manual resizing of frameless windows ---------------------------
1052 |
1053 | --this is a helper also used in backends.
1054 | function app:_resize_area_hit(mx, my, w, h, ho, vo, co)
1055 | if box2d.hit(mx, my, box2d.offset(co, 0, 0, 0, 0)) then
1056 | return 'topleft'
1057 | elseif box2d.hit(mx, my, box2d.offset(co, w, 0, 0, 0)) then
1058 | return 'topright'
1059 | elseif box2d.hit(mx, my, box2d.offset(co, 0, h, 0, 0)) then
1060 | return 'bottomleft'
1061 | elseif box2d.hit(mx, my, box2d.offset(co, w, h, 0, 0)) then
1062 | return 'bottomright'
1063 | elseif box2d.hit(mx, my, box2d.offset(ho, 0, 0, w, 0)) then
1064 | return 'top'
1065 | elseif box2d.hit(mx, my, box2d.offset(ho, 0, h, w, 0)) then
1066 | return 'bottom'
1067 | elseif box2d.hit(mx, my, box2d.offset(vo, 0, 0, 0, h)) then
1068 | return 'left'
1069 | elseif box2d.hit(mx, my, box2d.offset(vo, w, 0, 0, h)) then
1070 | return 'right'
1071 | end
1072 | end
1073 |
1074 | function window:_hittest(mx, my)
1075 | local where
1076 | if self:_can_set_rect() and self:resizeable() then
1077 | local ho, vo = 8, 8 --TODO: expose these?
1078 | local co = vo + ho --...and this (corner radius)
1079 | local w, h = self:client_size()
1080 | where = app:_resize_area_hit(mx, my, w, h, ho, vo, co)
1081 | end
1082 | local where1 = self:fire('hittest', mx, my, where)
1083 | if where1 ~= nil then where = where1 end
1084 | return where
1085 | end
1086 |
1087 | function window:_init_manual_resize()
1088 | if self:frame() ~= 'none' then return end
1089 |
1090 | local resizing, where, sides, dx, dy
1091 |
1092 | self:on('mousedown', function(self, button, mx, my)
1093 | if not (where and button == 'left') then return end
1094 | resizing = true
1095 | sides = {}
1096 | for _,side in ipairs{'left', 'top', 'right', 'bottom'} do
1097 | sides[side] = where:find(side, 1, true) and true or false
1098 | end
1099 | local cw, ch = self:client_size()
1100 | if where == 'move' then
1101 | dx, dy = -mx, -my
1102 | if app:ver'X' then
1103 | self:cursor'move'
1104 | end
1105 | else
1106 | dx = sides.left and -mx or cw - mx
1107 | dy = sides.top and -my or ch - my
1108 | end
1109 | self:_backend_sizing('start', where)
1110 | return true
1111 | end)
1112 |
1113 | self:on('mousemove', function(self, mx, my)
1114 | if not resizing then
1115 | local where0 = where
1116 | where = self:_hittest(mx, my)
1117 | if where and where ~= 'move' then
1118 | self:cursor(where)
1119 | elseif where0 then
1120 | self:cursor'arrow'
1121 | end
1122 | if where then
1123 | return true
1124 | end
1125 | else
1126 | mx, my = app:mouse'pos' --need absolute pos because X is async
1127 | if where == 'move' then
1128 | local w, h = self:client_size()
1129 | local x, y, w, h = self:_backend_sizing(
1130 | 'progress', where, mx + dx, my + dy, w, h)
1131 | self:frame_rect(x, y, w, h)
1132 | else
1133 | local x1, y1, x2, y2 = box2d.corners(self:frame_rect())
1134 | if sides.left then x1 = mx + dx end
1135 | if sides.right then x2 = mx + dx end
1136 | if sides.top then y1 = my + dy end
1137 | if sides.bottom then y2 = my + dy end
1138 | local x, y, w, h = self:_backend_sizing(
1139 | 'progress', where, box2d.rect(x1, y1, x2, y2))
1140 | self:frame_rect(x, y, w, h)
1141 | end
1142 | return true
1143 | end
1144 | end)
1145 |
1146 | self:on('mouseup', function(self, button, x, y)
1147 | if not resizing then return end
1148 | self:cursor'arrow'
1149 | resizing = false
1150 | self:_backend_sizing('end', where)
1151 | return true
1152 | end)
1153 | end
1154 |
1155 | --positioning/edge snapping --------------------------------------------------
1156 |
1157 | function window:_backend_sizing(when, how, x, y, w, h)
1158 |
1159 | if when ~= 'progress' then
1160 | self._magnets = nil
1161 | self:fire('sizing', when, how)
1162 | return
1163 | end
1164 |
1165 | local x1, y1, w1, h1
1166 |
1167 | if self:edgesnapping() then
1168 | self._magnets = self._magnets or self:_getmagnets()
1169 | if how == 'move' then
1170 | x1, y1 = box2d.snap_pos(20, x, y, w, h, self._magnets, true)
1171 | else
1172 | x1, y1, w1, h1 = box2d.snap_edges(20, x, y, w, h, self._magnets, true)
1173 | end
1174 | x1, y1, w1, h1 = override_rect(x, y, w, h, x1, y1, w1, h1)
1175 | else
1176 | x1, y1, w1, h1 = x, y, w, h
1177 | end
1178 |
1179 | local t = {x = x1, y = y1, w = w1, h = h1}
1180 | self:fire('sizing', when, how, t)
1181 | return override_rect(x1, y1, w1, h1, t.x, t.y, t.w, t.h)
1182 | end
1183 |
1184 | function window:edgesnapping(mode)
1185 | self:_check()
1186 | if mode == nil then
1187 | return self._edgesnapping
1188 | else
1189 | if mode == true then
1190 | mode = 'screen'
1191 | end
1192 | if mode == 'all' then
1193 | mode = 'app other screen'
1194 | end
1195 | if self._edgesnapping ~= mode then
1196 | self._magnets = nil
1197 | self._edgesnapping = mode
1198 | end
1199 | end
1200 | end
1201 |
1202 | local modes = glue.index{'app', 'other', 'screen', 'parent', 'siblings'}
1203 |
1204 | function window:_getmagnets()
1205 | local mode = self:edgesnapping()
1206 |
1207 | --parse and check options
1208 | local opt = {}
1209 | for s in mode:gmatch'[%a]+' do
1210 | assert(modes[s], 'invalid option %s', s)
1211 | opt[s] = true
1212 | end
1213 |
1214 | --ask user for magnets
1215 | local t = self:fire('magnets', opt)
1216 | if t ~= nil then return t end
1217 |
1218 | --ask backend for magnets
1219 | if opt.app and opt.other then
1220 | t = self.backend:magnets()
1221 | elseif (opt.app or opt.parent or opt.siblings) and not opt.other then
1222 | t = {}
1223 | for i,win in ipairs(app:windows()) do
1224 | if win ~= self then
1225 | local x, y, w, h = win:frame_rect()
1226 | if x then
1227 | if opt.app
1228 | or (opt.parent and win == self:parent())
1229 | or (opt.siblings and win:parent() == self:parent())
1230 | then
1231 | t[#t+1] = {x = x, y = y, w = w, h = h}
1232 | end
1233 | end
1234 | end
1235 | end
1236 | elseif opt.other then
1237 | error'NYI' --TODO: magnets excluding app's windows
1238 | end
1239 | if opt.screen then
1240 | t = t or {}
1241 | for i,disp in ipairs(app:displays()) do
1242 | local x, y, w, h = disp:desktop_rect()
1243 | t[#t+1] = {x = x, y = y, w = w, h = h}
1244 | local x, y, w, h = disp:screen_rect()
1245 | t[#t+1] = {x = x, y = y, w = w, h = h}
1246 | end
1247 | end
1248 |
1249 | return t
1250 | end
1251 |
1252 | --z-order --------------------------------------------------------------------
1253 |
1254 | window:_property'topmost'
1255 |
1256 | function window:raise(relto)
1257 | self:_check()
1258 | if relto then relto:_check() end
1259 | self.backend:raise(relto)
1260 | end
1261 |
1262 | function window:lower(relto)
1263 | self:_check()
1264 | if relto then relto:_check() end
1265 | self.backend:lower(relto)
1266 | end
1267 |
1268 | --title ----------------------------------------------------------------------
1269 |
1270 | window:_property'title'
1271 |
1272 | --displays -------------------------------------------------------------------
1273 |
1274 | local display = {}
1275 |
1276 | function app:_display(backend)
1277 | return glue.update(backend, display)
1278 | end
1279 |
1280 | function display:screen_rect()
1281 | return self.x, self.y, self.w, self.h
1282 | end
1283 |
1284 | function display:desktop_rect()
1285 | return self.cx, self.cy, self.cw, self.ch
1286 | end
1287 |
1288 | function app:displays(arg)
1289 | if arg == '#' then
1290 | return self.backend:display_count()
1291 | end
1292 | return self.backend:displays()
1293 | end
1294 |
1295 | function app:main_display() --the display at (0,0)
1296 | return self.backend:main_display()
1297 | end
1298 |
1299 | function app:active_display() --the display which has the keyboard focus
1300 | return self.backend:active_display()
1301 | end
1302 |
1303 | function app:_backend_displays_changed()
1304 | self:fire'displays_changed'
1305 | end
1306 |
1307 | function window:display()
1308 | self:_check()
1309 | return self.backend:display()
1310 | end
1311 |
1312 | --cursors --------------------------------------------------------------------
1313 |
1314 | function window:cursor(name)
1315 | if name ~= nil then
1316 | if type(name) == 'boolean' then
1317 | if self._cursor_visible == name then return end
1318 | self._cursor_visible = name
1319 | else
1320 | if self._cursor == name then return end
1321 | self._cursor = name
1322 | end
1323 | self.backend:update_cursor()
1324 | else
1325 | return self._cursor, self._cursor_visible
1326 | end
1327 | end
1328 |
1329 | --frame ----------------------------------------------------------------------
1330 |
1331 | function window:frame() self:_check(); return self._frame end
1332 | function window:transparent() self:_check(); return self._transparent end
1333 | function window:corner_radius() self:_check(); return self._corner_radius end
1334 | function window:background_color() self:_check(); return self._background_color end
1335 | function window:minimizable() self:_check(); return self._minimizable end
1336 | function window:maximizable() self:_check(); return self._maximizable end
1337 | function window:closeable() self:_check(); return self._closeable end
1338 | function window:resizeable() self:_check(); return self._resizeable end
1339 | function window:fullscreenable() self:_check(); return self._fullscreenable end
1340 | function window:activable() self:_check(); return self._activable end
1341 | function window:sticky() self:_check(); return self._sticky end
1342 |
1343 | function window:hideonclose(hideonclose)
1344 | self:_check()
1345 | if hideonclose == nil then
1346 | return self._hideonclose
1347 | else
1348 | self._hideonclose = hideonclose and true or false
1349 | end
1350 | end
1351 |
1352 | function window:autoquit(autoquit)
1353 | self:_check()
1354 | if autoquit == nil then
1355 | return self._autoquit
1356 | else
1357 | self._autoquit = autoquit and true or false
1358 | end
1359 | end
1360 |
1361 | --parent ---------------------------------------------------------------------
1362 |
1363 | function window:parent()
1364 | self:_check()
1365 | return self._parent
1366 | end
1367 |
1368 | function window:children(filter)
1369 | if filter then
1370 | assert(filter == '#', 'invalid argument')
1371 | local n = 0
1372 | for i,win in ipairs(app:windows()) do
1373 | if win:parent() == self then
1374 | n = n + 1
1375 | end
1376 | end
1377 | return n
1378 | end
1379 | local t = {}
1380 | for i,win in ipairs(app:windows()) do
1381 | if win:parent() == self then
1382 | t[#t+1] = win
1383 | end
1384 | end
1385 | return t
1386 | end
1387 |
1388 | --keyboard -------------------------------------------------------------------
1389 |
1390 | function app:ignore_numlock(ignore)
1391 | if ignore == nil then
1392 | return self._ignore_numlock
1393 | else
1394 | self._ignore_numlock = ignore
1395 | end
1396 | end
1397 |
1398 | --merge virtual key names into ambiguous key names.
1399 | local common_keynames = {
1400 | lshift = 'shift', rshift = 'shift',
1401 | lctrl = 'ctrl', rctrl = 'ctrl',
1402 | lalt = 'alt', ralt = 'alt',
1403 | lcommand = 'command', rcommand = 'command',
1404 |
1405 | ['left!'] = 'left', numleft = 'left',
1406 | ['up!'] = 'up', numup = 'up',
1407 | ['right!'] = 'right', numright = 'right',
1408 | ['down!'] = 'down', numdown = 'down',
1409 | ['pageup!'] = 'pageup', numpageup = 'pageup',
1410 | ['pagedown!'] = 'pagedown', numpagedown = 'pagedown',
1411 | ['end!'] = 'end', numend = 'end',
1412 | ['home!'] = 'home', numhome = 'home',
1413 | ['insert!'] = 'insert', numinsert = 'insert',
1414 | ['delete!'] = 'delete', numdelete = 'delete',
1415 | ['enter!'] = 'enter', numenter = 'enter',
1416 | }
1417 |
1418 | local function translate_key(vkey)
1419 | return common_keynames[vkey] or vkey, vkey
1420 | end
1421 |
1422 | function window:_backend_keydown(key)
1423 | return self:fire('keydown', translate_key(key))
1424 | end
1425 |
1426 | function window:_backend_keypress(key)
1427 | return self:fire('keypress', translate_key(key))
1428 | end
1429 |
1430 | function window:_backend_keyup(key)
1431 | return self:fire('keyup', translate_key(key))
1432 | end
1433 |
1434 | function window:_backend_keychar(s)
1435 | self:fire('keychar', s)
1436 | end
1437 |
1438 | --TODO: implement `key_pressed_now` arg and use it in `ui_editbox`!
1439 | function app:key(keys, key_pressed_now)
1440 | keys = keys:lower()
1441 | if keys:find'[^%+]%+' then --'alt+f3' -> 'alt f3'; 'ctrl++' -> 'ctrl +'
1442 | keys = keys:gsub('([^%+%s])%+', '%1 ')
1443 | end
1444 | if keys:find(' ', 1, true) then --it's a sequence, eg. 'alt f3'
1445 | local found
1446 | for _not, key in keys:gmatch'(!?)([^%s]+)' do
1447 | local wanted_response = _not == ''
1448 | if self.backend:key(key) ~= wanted_response then
1449 | return false
1450 | end
1451 | found = true
1452 | end
1453 | return assert(found, 'invalid key sequence')
1454 | end
1455 | return self.backend:key(keys)
1456 | end
1457 |
1458 | --mouse ----------------------------------------------------------------------
1459 |
1460 | function app:mouse(var)
1461 | if var == 'inside' then
1462 | return true
1463 | elseif var == 'pos' then
1464 | return self.backend:get_mouse_pos()
1465 | elseif var == 'x' then
1466 | return (self.backend:get_mouse_pos())
1467 | elseif var == 'y' then
1468 | return select(2, self.backend:get_mouse_pos())
1469 | end
1470 | end
1471 |
1472 | function app:double_click_time()
1473 | return self.backend:double_click_time()
1474 | end
1475 |
1476 | function app:double_click_target_area()
1477 | return self.backend:double_click_target_area()
1478 | end
1479 |
1480 | function app:caret_blink_time()
1481 | return self.backend:caret_blink_time()
1482 | end
1483 |
1484 | function window:mouse(var)
1485 | if not self:_can_get_rect() then return end
1486 | local inside = self._mouse.inside
1487 | if var == 'inside' then
1488 | return inside
1489 | elseif not (
1490 | inside
1491 | or self._mouse.left
1492 | or self._mouse.right
1493 | or self._mouse.middle
1494 | or self._mouse.x1
1495 | or self._mouse.x2
1496 | ) then
1497 | return --can only get mouse state when inside or captured
1498 | elseif var == 'pos' then
1499 | return self._mouse.x, self._mouse.y
1500 | else
1501 | return self._mouse[var]
1502 | end
1503 | end
1504 |
1505 | function window:_backend_mousedown(button, mx, my)
1506 | local t = self._down[button]
1507 | if not t then
1508 | t = {count = 0}
1509 | self._down[button] = t
1510 | end
1511 |
1512 | if t.count > 0
1513 | and time.clock() - t.time < t.interval
1514 | and box2d.hit(mx, my, t.x, t.y, t.w, t.h)
1515 | then
1516 | t.count = t.count + 1
1517 | t.time = time.clock()
1518 | else
1519 | t.count = 1
1520 | t.time = time.clock()
1521 | t.interval = app.backend:double_click_time()
1522 | t.w, t.h = app.backend:double_click_target_area()
1523 | t.x = mx - t.w / 2
1524 | t.y = my - t.h / 2
1525 | end
1526 |
1527 | self:fire('mousedown', button, mx, my, t.count)
1528 |
1529 | if self:fire('click', button, t.count, mx, my) then
1530 | t.count = 0
1531 | end
1532 | end
1533 |
1534 | function window:_backend_mouseup(button, x, y)
1535 | local t = self._down[button]
1536 | self:fire('mouseup', button, x, y, t and t.count or 0)
1537 | end
1538 |
1539 | function window:_backend_mouseenter(x, y)
1540 | self:fire('mouseenter', x, y)
1541 | end
1542 |
1543 | function window:_backend_mouseleave()
1544 | self:fire'mouseleave'
1545 | end
1546 |
1547 | function window:_backend_mousemove(x, y)
1548 | self:fire('mousemove', x, y)
1549 | end
1550 |
1551 | function window:_backend_mousewheel(delta, x, y, pixeldelta)
1552 | self:fire('mousewheel', delta, x, y, pixeldelta)
1553 | end
1554 |
1555 | function window:_backend_mousehwheel(delta, x, y, pixeldelta)
1556 | self:fire('mousehwheel', delta, x, y, pixeldelta)
1557 | end
1558 |
1559 | --rendering ------------------------------------------------------------------
1560 |
1561 | local count_per_sec = 2
1562 | local frame_count, last_frame_count, last_time = 0, 0
1563 | function app:fps()
1564 | last_time = last_time or time.clock()
1565 | frame_count = frame_count + 1
1566 | local time = time.clock()
1567 | if time - last_time > 1 / count_per_sec then
1568 | last_frame_count, frame_count = frame_count, 0
1569 | last_time = time
1570 | end
1571 | return last_frame_count * count_per_sec
1572 | end
1573 |
1574 | function window:invalidate(invalid_clock)
1575 | self._invalid_clock =
1576 | math.min(invalid_clock or -1/0, self._invalid_clock or 1/0)
1577 | self.backend:invalidate()
1578 | end
1579 |
1580 | function window:invalid(at_clock)
1581 | return (at_clock or time.clock()) >= (self._invalid_clock or 1/0)
1582 | end
1583 |
1584 | function window:validate(at_clock)
1585 | at_clock = at_clock or time.clock()
1586 | if self:invalid(at_clock) then
1587 | self._invalid_clock = 1/0
1588 | self._painted = false
1589 | self:fire'sync'
1590 | end
1591 | end
1592 |
1593 | function window:_backend_repaint()
1594 | if not self:_can_get_rect() then return end
1595 | self._painted = true
1596 | self:fire'repaint'
1597 | end
1598 |
1599 | function window:_backend_needs_repaint(at_clock)
1600 | self:validate(at_clock)
1601 | return not self._painted
1602 | end
1603 |
1604 | --bitmap
1605 |
1606 | local bitmap = {}
1607 |
1608 | function bitmap:clear()
1609 | ffi.fill(self.data, self.size)
1610 | end
1611 |
1612 | function window:bitmap()
1613 | assert(not self:opengl(), 'bitmap not available on OpenGL window/view')
1614 | local bmp = self.backend:bitmap()
1615 | return bmp and glue.update(bmp, bitmap)
1616 | end
1617 |
1618 | --cairo
1619 |
1620 | function bitmap:cairo()
1621 | local cairo = require'cairo'
1622 | if not self.cairo_surface then
1623 | self.cairo_surface = cairo.image_surface(self)
1624 | self.cairo_context = self.cairo_surface:context()
1625 | end
1626 | return self.cairo_context
1627 | end
1628 |
1629 | function window:_backend_free_bitmap(bitmap)
1630 | if bitmap.cairo_context then
1631 | self:fire('free_cairo', bitmap.cairo_context)
1632 | bitmap.cairo_context:free(); bitmap.cairo_context = nil
1633 | bitmap.cairo_surface:free(); bitmap.cairo_surface = nil
1634 | end
1635 | self:fire('free_bitmap', bitmap)
1636 | end
1637 |
1638 | --opengl
1639 |
1640 | function window:opengl(opt)
1641 | self:_check()
1642 | if not opt then
1643 | return self._opengl and true or false
1644 | end
1645 | assert(self._opengl, 'OpenGL not enabled')
1646 | local val = self._opengl[opt]
1647 | assert(val ~= nil, 'invalid option')
1648 | return val
1649 | end
1650 |
1651 | function window:gl()
1652 | assert(self:opengl(), 'OpenGL not enabled')
1653 | return self.backend:gl()
1654 | end
1655 |
1656 | --hi-dpi support -------------------------------------------------------------
1657 |
1658 | function app:autoscaling(enabled)
1659 | if enabled == nil then
1660 | return self.backend:get_autoscaling()
1661 | end
1662 | if enabled then
1663 | self.backend:enable_autoscaling()
1664 | else
1665 | self.backend:disable_autoscaling()
1666 | end
1667 | end
1668 |
1669 | function window:_backend_scalingfactor_changed(scalingfactor)
1670 | self:fire('scalingfactor_changed', scalingfactor)
1671 | end
1672 |
1673 | --views ----------------------------------------------------------------------
1674 |
1675 | local defaults = {
1676 | anchors = 'lt',
1677 | }
1678 |
1679 | local view = glue.update({}, object)
1680 |
1681 | function window:views(arg)
1682 | if arg == '#' then
1683 | return #self._views
1684 | end
1685 | return glue.extend({}, self._views) --take a snapshot; creation order.
1686 | end
1687 |
1688 | function window:view(t)
1689 | assert(not self:opengl(),
1690 | 'cannot create view over OpenGL-enabled window') --OSX limitation
1691 | return view:_new(self, self.backend.view, t)
1692 | end
1693 |
1694 | function view:_new(window, backend_class, useropt)
1695 |
1696 | local opt = glue.update({}, defaults, useropt)
1697 | opt.opengl = opengl_options(useropt.opengl)
1698 |
1699 | assert(opt.x and opt.y and opt.w and opt.h, 'x, y, w, h expected')
1700 | opt.w = math.max(1, opt.w) --avoid zero sizes
1701 | opt.h = math.max(1, opt.h)
1702 |
1703 | local self = glue.update({
1704 | window = window,
1705 | app = window.app,
1706 | }, self)
1707 |
1708 | self._mouse = {inside = false}
1709 | self._down = {}
1710 | self._anchors = opt.anchors
1711 | self._opengl = opt.opengl
1712 |
1713 | self.backend = backend_class:new(window.backend, self, opt)
1714 | table.insert(window._views, self)
1715 |
1716 | self:_init_anchors()
1717 |
1718 | if opt.visible ~= false then
1719 | self:show()
1720 | end
1721 |
1722 | return self
1723 | end
1724 |
1725 | function window:_free_views()
1726 | while #self._views > 0 do
1727 | self._views[#self._views]:free()
1728 | end
1729 | end
1730 |
1731 | function view:free()
1732 | if self._dead then return end
1733 | self:fire'freeing'
1734 | self.backend:free()
1735 | self._dead = true
1736 | table.remove(self.window._views, indexof(self, self.window._views))
1737 | end
1738 |
1739 | function view:visible(visible)
1740 | if visible ~= nil then
1741 | if visible then
1742 | self:show()
1743 | else
1744 | self:hide()
1745 | end
1746 | else
1747 | return self.backend:visible()
1748 | end
1749 | end
1750 |
1751 | function view:show()
1752 | self.backend:show()
1753 | end
1754 |
1755 | function view:hide()
1756 | self.backend:hide()
1757 | end
1758 |
1759 | --positioning
1760 |
1761 | function view:rect(x, y, w, h)
1762 | if x or y or w or h then
1763 | if not (x and y and w and h) then
1764 | x, y, w, h = override_rect(x, y, w, h, self.backend:get_rect())
1765 | end
1766 | w = math.max(1, w) --avoid zero sizes
1767 | h = math.max(1, h)
1768 | self.backend:set_rect(x, y, w, h)
1769 | else
1770 | return self.backend:get_rect()
1771 | end
1772 | end
1773 |
1774 | function view:size(w, h)
1775 | if w or h then
1776 | if not (w and h) then
1777 | local w0, h0 = self:size()
1778 | w = w or w0
1779 | h = h or h0
1780 | end
1781 | self.backend:set_size(w, h)
1782 | else
1783 | return select(3, self.backend:get_rect())
1784 | end
1785 | end
1786 |
1787 | function view:to_screen(x, y)
1788 | self:_check()
1789 | local x0, y0 = self.window:_get_client_pos()
1790 | if not x0 then return end
1791 | local cx, cy = self.backend:get_rect()
1792 | return x0+cx+x, y0+cy+y
1793 | end
1794 |
1795 | function view:to_client(x, y)
1796 | self:_check()
1797 | local x0, y0 = self.window:_get_client_pos()
1798 | if not x0 then return end
1799 | local cx, cy = self.backend:get_rect()
1800 | return x-cx-x0, y-cy-y0
1801 | end
1802 |
1803 | --anchors
1804 |
1805 | function view:anchors(a)
1806 | if a ~= nil then
1807 | self._anchors = a
1808 | else
1809 | return self._anchors
1810 | end
1811 | end
1812 |
1813 | function view:_init_anchors()
1814 | self._rect = {self:rect()}
1815 |
1816 | local function anchor(left, right, w, dx1, dx2, pw)
1817 | if left then
1818 | if right then --resize to preserve the right margin
1819 | return dx1, pw - dx2 - dx1
1820 | end
1821 | elseif right then --move to preserve right margin
1822 | return pw - w - dx2, w
1823 | end
1824 | return dx1, w
1825 | end
1826 |
1827 | local function has(s)
1828 | return self._anchors:find(s, 1, true)
1829 | end
1830 |
1831 | self.window:on('client_resized',
1832 | function(window, cw, ch, cw0, ch0)
1833 | local dx1, dy1, w, h = unpack(self._rect)
1834 | local dx2 = cw0 - w - dx1
1835 | local dy2 = ch0 - h - dy1
1836 | local x, w = anchor(has'l', has'r', w, dx1, dx2, cw)
1837 | local y, h = anchor(has't', has'b', h, dy1, dy2, ch)
1838 | self:rect(x, y, w, h)
1839 | end)
1840 | end
1841 |
1842 | --events
1843 |
1844 | function view:_can_get_rect()
1845 | return self.window:_can_get_rect()
1846 | end
1847 |
1848 | view._rect_changed = window._rect_changed
1849 |
1850 | function view:_backend_changed()
1851 | self._rect = self:_rect_changed(self._rect, {self:rect()},
1852 | 'rect_changed', 'moved', 'resized')
1853 | end
1854 |
1855 | --mouse
1856 |
1857 | view.mouse = window.mouse
1858 | view._backend_mousedown = window._backend_mousedown
1859 | view._backend_mouseup = window._backend_mouseup
1860 | view._backend_mouseenter = window._backend_mouseenter
1861 | view._backend_mouseleave = window._backend_mouseleave
1862 | view._backend_mousemove = window._backend_mousemove
1863 | view._backend_mousewheel = window._backend_mousewheel
1864 | view._backend_mousehwheel = window._backend_mousehwheel
1865 |
1866 | --rendering
1867 |
1868 | view.bitmap = window.bitmap
1869 | view.cairo = window.cairo
1870 | view.opengl = window.opengl
1871 | view.gl = window.gl
1872 | view.invalidate = window.invalidate
1873 | view.invalid = window.invalid
1874 | view.validate = window.validate
1875 | view._backend_repaint = window._backend_repaint
1876 | view._backend_needs_repaint = window._backend_needs_repaint
1877 | view._backend_free_bitmap = window._backend_free_bitmap
1878 |
1879 | --menus ----------------------------------------------------------------------
1880 |
1881 | local menu = glue.update({}, object)
1882 |
1883 | local function wrap_menu(backend, menutype)
1884 | if backend.frontend then
1885 | return backend.frontend --already wrapped
1886 | end
1887 | local self = glue.update({backend = backend, menutype = menutype}, menu)
1888 | backend.frontend = self
1889 | return self
1890 | end
1891 |
1892 | function app:menu(menu)
1893 | return wrap_menu(self.backend:menu(), 'menu')
1894 | end
1895 |
1896 | function app:menubar()
1897 | return wrap_menu(self.backend:menubar(), 'menubar')
1898 | end
1899 |
1900 | function window:menubar()
1901 | return wrap_menu(self.backend:menubar(), 'menubar')
1902 | end
1903 |
1904 | function window:popup(menu, x, y)
1905 | return self.backend:popup(menu, x, y)
1906 | end
1907 |
1908 | function view:popup(menu, x, y)
1909 | local vx, vy = self:rect()
1910 | return self.window:popup(menu, vx + x, vy + y)
1911 | end
1912 |
1913 | function menu:popup(target, x, y)
1914 | return target:popup(self, x, y)
1915 | end
1916 |
1917 | function menu:_parseargs(index, text, action, options)
1918 | local args = {}
1919 |
1920 | --args can have the form:
1921 | -- ([index, ]text, [action], [options])
1922 | -- {index=, text=, action=, optionX=...}
1923 | if type(index) == 'table' then
1924 | args = glue.update({}, index)
1925 | index = args.index
1926 | elseif type(index) ~= 'number' then
1927 | index, args.text, args.action, options = nil, index, text, action --index is optional
1928 | else
1929 | args.text, args.action = text, action
1930 | end
1931 |
1932 | --default text is empty, i.e. separator.
1933 | args.text = args.text or ''
1934 |
1935 | --action can be a function or a submenu.
1936 | if type(args.action) == 'table' and args.action.menutype then
1937 | args.action, args.submenu = nil, args.action
1938 | end
1939 |
1940 | --options add to the sequential args but don't override them.
1941 | glue.merge(args, options)
1942 |
1943 | --a title made of zero or more '-' means separator (not for menu bars).
1944 | if self.menutype ~= 'menubar' and args.text:find'^%-*$' then
1945 | args.separator = true
1946 | args.text = ''
1947 | args.action = nil
1948 | args.submenu = nil
1949 | args.enabled = true
1950 | args.checked = false
1951 | else
1952 | if args.enabled == nil then args.enabled = true end
1953 | if args.checked == nil then args.checked = false end
1954 | end
1955 |
1956 | --the title can be followed by two or more spaces and then by a shortcut.
1957 | local shortcut = args.text:reverse():match'^%s*(.-)%s%s'
1958 | if shortcut then
1959 | args.shortcut = shortcut:reverse()
1960 | args.text = text
1961 | end
1962 |
1963 | return index, args
1964 | end
1965 |
1966 | function menu:add(...)
1967 | return self.backend:add(self:_parseargs(...))
1968 | end
1969 |
1970 | function menu:set(...)
1971 | self.backend:set(self:_parseargs(...))
1972 | end
1973 |
1974 | function menu:remove(index)
1975 | self.backend:remove(index)
1976 | end
1977 |
1978 | function menu:get(index, var)
1979 | if var then
1980 | local item = self.backend:get(index)
1981 | return item and item[var]
1982 | else
1983 | return self.backend:get(index)
1984 | end
1985 | end
1986 |
1987 | function menu:items(var)
1988 | if var == '#' then
1989 | return self.backend:item_count()
1990 | end
1991 | local t = {}
1992 | for i = 1, self:items'#' do
1993 | t[i] = self:get(i, var)
1994 | end
1995 | return t
1996 | end
1997 |
1998 | function menu:checked(i, checked)
1999 | if checked == nil then
2000 | return self.backend:get_checked(i)
2001 | else
2002 | self.backend:set_checked(i, checked)
2003 | end
2004 | end
2005 |
2006 | function menu:enabled(i, enabled)
2007 | if enabled == nil then
2008 | return self.backend:get_enabled(i)
2009 | else
2010 | self.backend:set_enabled(i, enabled)
2011 | end
2012 | end
2013 |
2014 | --notification icons ---------------------------------------------------------
2015 |
2016 | local notifyicon = glue.update({}, object)
2017 |
2018 | function app:notifyicon(opt)
2019 | local icon = notifyicon:_new(self, self.backend.notifyicon, opt)
2020 | table.insert(self._notifyicons, icon)
2021 | return icon
2022 | end
2023 |
2024 | function notifyicon:_new(app, backend_class, opt)
2025 | self = glue.update({app = app}, self)
2026 | self.backend = backend_class:new(app.backend, self, opt)
2027 | return self
2028 | end
2029 |
2030 | function notifyicon:free()
2031 | if self._dead then return end
2032 | self.backend:free()
2033 | self._dead = true
2034 | table.remove(app._notifyicons, indexof(self, app._notifyicons))
2035 | end
2036 |
2037 | function app:_free_notifyicons() --called on app:quit()
2038 | while #self._notifyicons > 0 do
2039 | self._notifyicons[#self._notifyicons]:free()
2040 | end
2041 | end
2042 |
2043 | function app:notifyicons(arg)
2044 | if arg == '#' then
2045 | return #self._notifyicons
2046 | end
2047 | return glue.extend({}, self._notifyicons) --take a snapshot
2048 | end
2049 |
2050 | function notifyicon:bitmap()
2051 | self:_check()
2052 | return self.backend:bitmap()
2053 | end
2054 |
2055 | function notifyicon:invalidate()
2056 | return self.backend:invalidate()
2057 | end
2058 |
2059 | function notifyicon:_backend_repaint()
2060 | self:fire'repaint'
2061 | end
2062 |
2063 | function notifyicon:_backend_free_bitmap(bitmap)
2064 | self:fire('free_bitmap', bitmap)
2065 | end
2066 |
2067 | notifyicon:_property'tooltip'
2068 | notifyicon:_property'menu'
2069 | notifyicon:_property'text' --OSX only
2070 | notifyicon:_property'length' --OSX only
2071 |
2072 | --window icon ----------------------------------------------------------------
2073 |
2074 | local winicon = glue.update({}, object)
2075 |
2076 | local function whicharg(which)
2077 | assert(which == nil or which == 'small' or which == 'big')
2078 | return which == 'small' and 'small' or 'big'
2079 | end
2080 |
2081 | function window:icon(which)
2082 | local which = whicharg(which)
2083 | if self:frame() == 'toolbox' then return end --toolboxes don't have icons
2084 | self._icons = self._icons or {}
2085 | if not self._icons[which] then
2086 | self._icons[which] = winicon:_new(self, which)
2087 | end
2088 | return self._icons[which]
2089 | end
2090 |
2091 | function winicon:_new(window, which)
2092 | self = glue.update({}, winicon)
2093 | self.window = window
2094 | self.which = which
2095 | return self
2096 | end
2097 |
2098 | function winicon:bitmap()
2099 | return self.window.backend:icon_bitmap(self.which)
2100 | end
2101 |
2102 | function winicon:invalidate()
2103 | return self.window.backend:invalidate_icon(self.which)
2104 | end
2105 |
2106 | function window:_backend_repaint_icon(which)
2107 | which = whicharg(which)
2108 | self._icons[which]:fire('repaint')
2109 | end
2110 |
2111 | --dock icon ------------------------------------------------------------------
2112 |
2113 | local dockicon = glue.update({}, object)
2114 |
2115 | function app:dockicon()
2116 | if not self._dockicon then
2117 | self._dockicon = dockicon:_new(self)
2118 | end
2119 | return self._dockicon
2120 | end
2121 |
2122 | function dockicon:_new(app)
2123 | return glue.update({app = app}, self)
2124 | end
2125 |
2126 | function dockicon:bitmap()
2127 | return app.backend:dockicon_bitmap()
2128 | end
2129 |
2130 | function dockicon:invalidate()
2131 | app.backend:dockicon_invalidate()
2132 | end
2133 |
2134 | function app:_free_dockicon()
2135 | if not self.backend.dockicon_free then return end --only on OSX
2136 | self.backend:dockicon_free()
2137 | end
2138 |
2139 | function app:_backend_dockicon_repaint()
2140 | self._dockicon:fire'repaint'
2141 | end
2142 |
2143 | function app:_backend_dockicon_free_bitmap(bitmap)
2144 | self._dockicon:fire('free_bitmap', bitmap)
2145 | end
2146 |
2147 | --file chooser ---------------------------------------------------------------
2148 |
2149 | --TODO: make default filetypes = {'*'} and add '*' filetype to indicate "all others".
2150 |
2151 | local defaults = {
2152 | title = nil,
2153 | filetypes = nil, --{'png', 'txt', ...}; first is default
2154 | multiselect = false,
2155 | initial_dir = nil,
2156 | }
2157 |
2158 | function app:opendialog(opt)
2159 | opt = glue.update({}, defaults, opt)
2160 | assert(not opt.filetypes or #opt.filetypes > 0, 'filetypes cannot be an empty list')
2161 | local paths = self.backend:opendialog(opt)
2162 | if not paths then return nil end
2163 | return opt.multiselect and paths or paths[1] or nil
2164 | end
2165 |
2166 | local defaults = {
2167 | title = nil,
2168 | filetypes = nil, --{'png', 'txt', ...}; first is default
2169 | filename = nil,
2170 | initial_dir = nil,
2171 | }
2172 |
2173 | function app:savedialog(opt)
2174 | opt = glue.update({}, defaults, opt)
2175 | assert(not opt.filetypes or #opt.filetypes > 0, 'filetypes cannot be an empty list')
2176 | return self.backend:savedialog(opt) or nil
2177 | end
2178 |
2179 | --clipboard ------------------------------------------------------------------
2180 |
2181 | function app:getclipboard(format)
2182 | if not format then
2183 | return self.backend:get_clipboard_formats()
2184 | else
2185 | return self.backend:get_clipboard_data(format)
2186 | end
2187 | end
2188 |
2189 | function app:setclipboard(data, format)
2190 | local t
2191 | if data == false then --clear clipboard
2192 | assert(format == nil)
2193 | elseif format == 'text' or (format == nil and type(data) == 'string') then
2194 | t = {{format = 'text', data = data}}
2195 | elseif format == 'files' and type(data) == 'table' then
2196 | t = {{format = 'files', data = data}}
2197 | elseif format == 'bitmap' or (format == nil and type(data) == 'table' and data.stride) then
2198 | t = {{format = 'bitmap', data = data}}
2199 | elseif format == nil and type(data) == 'table' and not data.stride then
2200 | t = data
2201 | else
2202 | error'invalid argument'
2203 | end
2204 | return self.backend:set_clipboard(t)
2205 | end
2206 |
2207 | --drag & drop ----------------------------------------------------------------
2208 |
2209 | function window:_backend_drop_files(x, y, files)
2210 | self:fire('dropfiles', x, y, files)
2211 | end
2212 |
2213 | local effect_arg = optarg({'copy', 'link', 'none', 'abort'}, 'copy', 'abort', 'abort')
2214 |
2215 | function window:_backend_dragging(stage, data, x, y)
2216 | return effect_arg(self:fire('dragging', how, data, x, y))
2217 | end
2218 |
2219 | --tooltips -------------------------------------------------------------------
2220 |
2221 | function window:tooltip(text)
2222 | if text ~= nil then
2223 | assert(text ~= true, 'false or string expected')
2224 | self.backend:set_tooltip(text) --false or 'text'
2225 | else
2226 | return self.backend:get_tooltip()
2227 | end
2228 | end
2229 |
2230 | return nw
2231 |
--------------------------------------------------------------------------------
/nw.md:
--------------------------------------------------------------------------------
1 |
2 | ## `local nw = require'nw'`
3 |
4 | Cross-platform library for accessing windows, graphics and input in a
5 | consistent manner across Windows, Linux and OS X. Supports transparent
6 | windows, bgra8 bitmaps everywhere, drawing via [cairo] and [opengl], edge
7 | snapping, fullscreen mode, multiple displays, hi-dpi, key mappings,
8 | triple-click events, timers, cursors, native menus, notification icons, all
9 | text in utf8, and more.
10 |
11 | ## Status
12 |
13 | See [issues](https://github.com/luapower/nw/issues)
14 | and [milestones](https://github.com/luapower/nw/milestones).
15 |
16 | ## Backends
17 |
18 | API Library Developed & Tested On Probably Runs On
19 | ---------- ----------- ------------------------ -----------------------
20 | WinAPI [winapi] Windows 7 x64 Windows XP/2000
21 | Cocoa [objc] OSX 10.12 OSX 10.9
22 | Xlib [xlib] Ubuntu/Unity 18.04 x64 Ubuntu/Unity 10.04
23 |
24 | ## Example
25 |
26 | ~~~{.lua}
27 | local nw = require'nw'
28 |
29 | local app = nw:app() --get the app singleton
30 |
31 | local win = app:window{ --create a new window
32 | w = 400, h = 200, --specify window's frame size
33 | title = 'hello', --specify window's title
34 | visible = false, --don't show it yet
35 | }
36 |
37 | function win:click(button, count) --this is one way to bind events
38 | if button == 'left' and count == 3 then --triple click
39 | app:quit()
40 | end
41 | end
42 |
43 | --this is another way to bind events which allows setting multiple
44 | --handlers for the same event type.
45 | win:on('keydown', function(self, key)
46 | if key == 'F11' then
47 | self:fullscreen(not self:fullscreen()) --toggle fullscreen state
48 | end
49 | end)
50 |
51 | function win:repaint() --called when window needs repainting
52 | local bmp = win:bitmap() --get the window's bitmap
53 | local cr = bmp:cairo() --get a cairo drawing context
54 | cr:rgb(0, 1, 0) --make it green
55 | cr:paint()
56 | end
57 |
58 | win:show() --show it now that it was properly set up
59 |
60 | app:run() --start the event loop
61 | ~~~
62 |
63 | ## API
64 |
65 | __NOTE:__ In the table below, `foo(t|f) /-> t|f` is a shortcut for saying
66 | that `foo(t|f)` sets the value of foo and `foo() -> t|f` gets it.
67 | `t|f` means `true|false`.
68 |
69 | -------------------------------------------- -----------------------------------------------------------------------------
70 | __the app object__
71 | `nw:app() -> app` the global application object
72 | __the app loop__
73 | `app:run()` start the loop
74 | `app:stop()` stop the loop
75 | `app:running() -> t|f` check if the loop is running
76 | `app:poll([timeout]) -> t|f` process the next pending event (return true if there was one)
77 | `app:maxfps(fps) -> fps` cap the window repaint rate
78 | __quitting__
79 | `app:quit()` quit the app, i.e. close all windows and stop the loop
80 | `app:autoquit(t|f) /-> t|f` quit the app when the last visible window is closed (`true`)
81 | `app:quitting() -> [false]` event: quitting (return `false` to refuse)
82 | `win:autoquit(t|f) /-> t|f` quit the app when the window is closed (`false`)
83 | __timers__
84 | `app:runevery(seconds, func)` run a function on a timer (return `false` to stop it)
85 | `app:runafter(seconds, func)` run a function on a timer once
86 | `app:run(func)` run a function on a zero-second timer once
87 | `app:sleep(seconds)` sleep without blocking an app:run() function
88 | __window tracking__
89 | `app:windows(['#',][filter]) -> {win1, ...}` all windows in creation order
90 | `app:window_created(win)` event: a window was created
91 | `app:window_closed(win)` event: a window was closed
92 | __window creation__
93 | `app:window(t|cw,ch,[title],[vis]) -> win` create a window
94 | __window closing__
95 | `win:close([reason|force])` close the window and hide it or destroy it
96 | `win:free([force])` close the window and destroy it
97 | `win:hideonclose(t|f) /-> t|f` hide on close or destroy on close
98 | `win:dead() -> t|f` check if the window was destroyed
99 | `win:closing(reason, [closing_win])` event: closing (return `false` to refuse)
100 | `win:closed()` event: window is about to be destroyed
101 | `win:closeable() -> t|f` closeable flag
102 | __window & app activation__
103 | `app/win:active() -> t|f` check if app/window is active
104 | `app:activate([mode])` activate the app
105 | `app:active_window() -> win` the active window, if any
106 | `win:activate()` activate the window
107 | `win:activable() -> t|f` activable flag
108 | `app/win:activated()` event: app/window was activated
109 | `app/win:deactivated()` event: app/window was deactivated
110 | __app instances__
111 | `app:check_single_instance()` single app instance check
112 | `app.id` set an app ID
113 | `app:already_running() -> t|f` check if other app instances running
114 | `app:wakeup_other_instances()` send wakeup event to other app instances
115 | `app:wakeup()` event: wakeup from another instance
116 | __app visibility (OSX)__
117 | `app:visible(t|f) /-> t|f` get/set app visibility
118 | `app:hide()` hide the app
119 | `app:unhide()` unhide the app
120 | `app:hidden()` event: app was hidden
121 | `app:unhidden()` event: app was unhidden
122 | __window state__
123 | `win:visible(t|f) /-> t|f` get/set window visibility
124 | `win:show()` show window (in its previous state)
125 | `win:hide()` hide window
126 | `win:shown()` event: window was shown
127 | `win:hidden()` event: window was hidden
128 | `win:minimizable() -> t|f` minimizable flag
129 | `win:isminimized() -> t|f` check if the window is minimized
130 | `win:minimize()` minimize the window
131 | `win:minimized()` event: window was minimized
132 | `win:unminimized()` event: window was unminimized
133 | `win:maximizable() -> t|f` maximizable flag
134 | `win:ismaximized() -> t|f` check if the window is maximized
135 | `win:maximize()` maximize the window
136 | `win:maximized()` event: window was maximized
137 | `win:unmaximized()` event: window was unmaximized
138 | `win:fullscreenable() -> t|f` fullscreenable flag
139 | `win:fullscreen(t|f) /-> t|f` get/enter/exit fullscreen mode
140 | `win:entered_fullscreen()` event: entered fullscreen mode
141 | `win:exited_fullscreen()` event: exited fullscreen mode
142 | `win:restore()` restore from minimized or maximized state
143 | `win:shownormal()` show in normal state
144 | `win:showmodal()` show as modal window
145 | `win:changed(old_state, new_state)` event: window state changed
146 | `app:changed(old_state, new_state)` event: app state changed
147 | `win:enabled(t|f) /-> t|f` get/set window enabled flag
148 | __frame extents__
149 | `app:frame_extents(...) -> ...` frame extents for a frame type
150 | `app:client_to_frame(...) -> ...` client rect -> window frame rect conversion
151 | `app:frame_to_client(...) -> ...` window frame rect -> client rect conversion
152 | __size and position__
153 | `win:client_rect(x,y,w,h) /-> x,y,w,h` get/set client rect
154 | `win:frame_rect(x,y,w,h) /-> x,y,w,h` get/set frame rect
155 | `win:client_size(cw, ch) /-> cw, ch` get/set client rect size
156 | `win/view:to_screen(x, y) -> x, y` client space -> screen space conversion
157 | `win/view:to_client(x, y) -> x, y` screen space -> client space conversion
158 | `win:normal_frame_rect() -> x,y,w,h` get frame rect in normal state
159 | `win:sizing(when, how, rect)` event: window size/position is about to change
160 | `win:frame_rect_changed(x, y, w, h, ...)` event: window frame was moved and/or resized
161 | `win:frame_moved(x, y, oldx, oldy)` event: window frame was moved
162 | `win:frame_resized(w, h, oldw, oldh)` event: window frame was resized
163 | `win:client_rect_changed(cx,cy,cw,ch,...)` event: window client area was moved and/or resized
164 | `win:client_moved(cx, cy, oldcx, oldcy)` event: window client area was moved
165 | `win:client_resized(cw, ch, oldcw, oldch)` event: window client area was resized
166 | `win:hittest(x, y) -> where` event: hit test for frameless windows
167 | __size constraints__
168 | `win:resizeable() -> t|f` resizeable flag
169 | `win:minsize(cw, ch) /-> cw, ch` get/set min client rect size
170 | `win:maxsize(cw, ch) /-> cw, ch` get/set max client rect size
171 | __window edge snapping__
172 | `win:edgesnapping(mode) /-> mode` get/set edge snapping mode
173 | `win:magnets(which) -> {r1, ...}` event: get edge snapping rectangles
174 | __window z-order__
175 | `win:topmost(t|f) /-> t|f` get/set the topmost flag
176 | `win:raise([rel_to_win])` raise above all windows/specific window
177 | `win:lower([rel_to_win])` lower below all windows/specific window
178 | __window title__
179 | `win:title(title) /-> title` get/set title
180 | __displays__
181 | `app:displays() -> {disp1, ...}` get displays (in no specific order)
182 | `app:main_display() -> disp ` the display whose screen rect starts at (0,0)
183 | `app:active_display() -> disp` the display which contains the active window
184 | `disp:screen_rect() -> x, y, w, h` display's screen rectangle
185 | `disp.x, disp.y, disp.w, disp.h`
186 | `disp:desktop_rect() -> cx, cy, cw, ch` display's screen rectangle minus the taskbar
187 | `disp.cx, disp.cy, disp.cw, disp.ch`
188 | `app:displays_changed()` event: displays changed
189 | `win:display() -> disp|nil` the display the window is on
190 | __cursors__
191 | `win:cursor(name|t|f) /-> name, t|f` get/set the mouse cursor and visibility
192 | `app:caret_blink_time() -> time | 1/0` caret blink time
193 | __frame flags__
194 | `win:frame() -> frame` window's frame: 'normal', 'none', 'toolbox'
195 | `win:transparent() -> t|f` transparent flag
196 | `win:corner_radius() -> n` rounded corners (0)
197 | __child windows__
198 | `win:parent() -> win|nil` window's parent
199 | `win:children() -> {win1, ...}` child windows
200 | `win:sticky() -> t|f` sticky flag
201 | __hi-dpi support__
202 | `app:autoscaling(t|f) /-> t|f` get/enable/disable autoscaling
203 | `disp.scalingfactor` display's scaling factor
204 | `win:scalingfactor_changed()` event: a window's display scaling factor changed
205 | __views__
206 | `win:views() -> {view1, ...}` list views
207 | `win:view(t) -> view` create a view
208 | `view:free()` destroy the view
209 | `view:dead() -> t|f` check if the view was freed
210 | `view:visible(t|f) /-> t|f` get/set view's visibility
211 | `view:show()` show the view
212 | `view:hide()` hide the view
213 | `view:rect(x, y, w, h) /-> x, y, w, h` get/set view's position (in window's client space) and size
214 | `view:size(w, h) /-> w, h` get/set view's size
215 | `view:anchors(anchors) /-> anchors` get/set anchors
216 | `view:rect_changed(x, y, w, h)` event: view's size and/or position changed
217 | `view:moved(x, y, oldx, oldy)` event: view was moved
218 | `view:resized(w, h, oldw, oldh)` event: view was resized
219 | __keyboard__
220 | `app:key(query) -> t|f` get key pressed and toggle states
221 | `win:keydown(key)` event: a key was pressed
222 | `win:keyup(key)` event: a key was depressed
223 | `win:keypress(key)` event: sent after each keydown, including repeats
224 | `win:keychar(s)` event: input char pressed; _s_ is utf-8
225 | __mouse__
226 | `app:mouse(var) -> val` mouse state: _x, y, pos_
227 | `win/view:mouse(var) -> val` mouse state: _x, y, pos, inside, left, right, middle, x1, x2_
228 | `win/view:mouseenter(x, y)` event: mouse entered the client area of the window
229 | `win/view:mouseleave()` event: mouse left the client area of the window
230 | `win/view:mousemove(x, y)` event: mouse was moved
231 | `win/view:mousedown(button, x, y, count)` event: mouse button was pressed
232 | `win/view:mouseup(button, x, y, count)` event: mouse button was depressed
233 | `win/view:click(button, count, x, y)` event: mouse button was clicked
234 | `win/view:mousewheel(delta, x, y, pdelta)` event: mouse wheel was moved
235 | `win/view:hmousewheel(delta, x, y, pdelta)` event: mouse horizontal wheel was moved
236 | `app:double_click_time() -> time` double click time
237 | `app:double_click_target_area() -> w, h` double click target area
238 | __rendering__
239 | `win/view:repaint()` event: needs repainting
240 | `win/view:sync()` event: needs sync'ing
241 | `win/view:invalidate([invalid_clock])` request sync'ing and repainting
242 | `win/view:validate([at_clock])` request sync'ing if invalid
243 | `win/view:invalid([at_clock]) -> t|f` check if invalidated
244 | `win/view:bitmap() -> bmp` get a bgra8 [bitmap] object to draw on
245 | `bmp:clear()` fill the bitmap with zero bytes
246 | `bmp:cairo() -> cr` get a cairo context on the bitmap
247 | `win/view:free_cairo(cr)` event: cairo context needs freeing
248 | `win/view:free_bitmap(bmp)` event: bitmap needs freeing
249 | `win/view:gl() -> gl` get an OpenGL context to draw with
250 | __menus__
251 | `app:menu() -> menu` create a menu (or menu bar)
252 | `app:menubar() -> menu` get app's menu bar (OSX)
253 | `win:menubar(menu|nil) /-> menu|nil` get/set/remove window's menu bar (Windows, Linux)
254 | `win/view:popup(menu, cx, cy)` pop up a menu relative to a window or view
255 | `menu:popup(win/view, cx, cy)` pop up a menu relative to a window or view
256 | `menu:add(...)`
257 | `menu:set(...)`
258 | `menu:remove(index)`
259 | `menu:get(index) -> item` get the menu item at index
260 | `menu:get(index, prop) -> val` get the value of a property of the menu item at index
261 | `menu:items([prop]) -> {item1, ...}`
262 | `menu:checked(index, t|f) /-> t|f` get/set a menu item's checked state
263 | __icons (common API)__
264 | `icon:free()`
265 | `icon:bitmap() -> bmp` get a bgra8 [bitmap] object
266 | `icon:invalidate()` request repainting
267 | `icon:repaint()` event: bitmap needs redrawing
268 | `icon:free_bitmap(bmp)` event: bitmap needs freeing
269 | __notification icons__
270 | `app:notifyicon(t) -> icon`
271 | `app:notifyicons() -> {icon1, ...}` list notification icons
272 | `icon:tooltip(s) /-> s` get/set icon's tooltip
273 | `icon:menu(menu) /-> menu` get/set icon's menu
274 | `icon:text(s) /-> s` get/set text (OSX)
275 | `icon:length(n) /-> n` get/set length (OSX)
276 | __window icon (Windows)__
277 | `win:icon([which]) -> icon` window's icon ('big'); which can be: 'big', 'small'
278 | __dock icon (OSX)__
279 | `app:dockicon() -> icon`
280 | __file choose dialogs__
281 | `app:opendialog(t) -> path|{path1,...}|nil` open a standard "open file" dialog
282 | `app:savedialog(t) -> path|nil` open a standard "save file" dialog
283 | __clipboard__
284 | `app:getclipboard(format) -> data|nil` get data in clipboard (format is 'text', 'files', 'bitmap')
285 | `app:getclipboard() -> formats` get data formats in clipboard
286 | `app:setclipboard(f|data[, format])` clear or set clipboard
287 | __drag & drop__
288 | `win/view:dropfiles(x, y, files)` event: files are dropped
289 | `win/view:dragging('enter',t,x,y) -> s` event: mouse enter with payload
290 | `win/view:dragging('hover',t,x,y) -> s` event: mouse move with payload
291 | `win/view:dragging('drop',t,x,y)` event: dropped the payload
292 | `win/view:dragging('leave')` event: mouse left with payload
293 | __tooltips__
294 | `win:tooltip(text|f) -> text|f` get/set/hide tooltip text
295 | __events__
296 | `app/win/view:on(event, func)` call _func_ when _event_ happens
297 | `app/win/view:off(event)` remove event handlers
298 | `app/win/view:fire(event, args...) -> ret` fire an event
299 | `app/win/view:events(enabled) -> prev_state` enable/disable events
300 | `app/win/view:event(name, args...)` meta-event fired on every other event
301 | __version checks__
302 | `app:ver(query) -> t|f` check OS _minimum_ version (eg. 'OSX 10.8')
303 | __extending__
304 | `nw.backends -> {os -> module_name}` default backend modules for each OS
305 | `nw:init([backend_name])` init with a specific backend (can be called only once)
306 | -------------------------------------------- -----------------------------------------------------------------------------
307 |
308 | ## The app object
309 |
310 | The global app object is the API from which everything else gets created.
311 |
312 | ### `nw:app() -> app`
313 |
314 | Get the global application object.
315 |
316 | This calls `nw:init()` which initializes the library with the default
317 | backend for the current platform.
318 |
319 | ## The app loop
320 |
321 | ### `app:run()`
322 |
323 | Start the application main loop.
324 |
325 | Calling run() when the loop is already running does nothing.
326 |
327 | ### `app:stop()`
328 |
329 | Stop the loop.
330 |
331 | Calling stop() when the loop is not running does nothing.
332 |
333 | ### `app:running() -> t|f`
334 |
335 | Check if the loop is running.
336 |
337 | ### `app:poll([timeout]) -> t|f`
338 |
339 | Process the next pending event from the event queue.
340 | Returns `true` if there was an event to process, `false` if there wasn't.
341 | Returns `false, exit_code` if the application was asked to quit.
342 | `timeout` (default=0) specifies a maximum wait time for an event to appear.
343 |
344 | ### `app:maxfps(fps)` `app:maxfps() -> fps`
345 |
346 | Get/set the maximum window repaint rate (frames per second).
347 | `1/0` disables the throttling. The default is `60`. Note that you still need
348 | to call `invalidate()` in order to trigger a repaint.
349 |
350 | ## Quitting
351 |
352 | ### `app:quit()`
353 |
354 | Quit the app, i.e. close all windows and stop the loop.
355 |
356 | Quitting is a multi-phase process:
357 |
358 | 1. `app:quitting()` event is fired. If it returns `false`, quitting is aborted.
359 | 2. `win:closing('quit', closing_win)` event is fired on all non-child windows,
360 | with the initial window as arg#2. If any of them returns `false`, quitting
361 | is aborted.
362 | 3. `win:free'force'` is called on all windows (in reverse-creation order).
363 | 4. the app loop is stopped.
364 |
365 | Calling `quit()` when the loop is not running or while quitting
366 | is in progress does nothing.
367 |
368 | ### `app:autoquit() -> t|f` `app:autoquit(t|f)`
369 |
370 | Get/set the app autoquit flag (default: `true`).
371 | When this flag is `true`, the app loop exists when the last visible non-child
372 | window is closed.
373 |
374 | ### `app:quitting() -> [false]`
375 |
376 | Event: the app wants to quit, but nothing was done to that effect.
377 | Return `false` from this event to cancel the process.
378 |
379 | ### `win:autoquit() -> t|f` `win:autoquit(t|f)`
380 |
381 | Get/set the window autoquit flag (default: `false`).
382 | When this flag is `true`, the app loop exists when the window is closed.
383 | This flag can be used on the app's main window if there is such a thing.
384 |
385 | ## Timers
386 |
387 | ### `app:runevery(seconds, func)`
388 |
389 | Run a function on a recurrent timer.
390 | The timer can be stopped by returning `false` from the function.
391 |
392 | ### `app:runafter(seconds, func)`
393 |
394 | Run a function on a timer once.
395 |
396 | ### `app:run(func)`
397 |
398 | Run a function on a zero-second timer, once, inside a coroutine.
399 | This allows calling `app:sleep()` inside the function (see below).
400 |
401 | If the loop is not already started, it is started and then stopped after
402 | the function finishes.
403 |
404 | ### `app:sleep(seconds)`
405 |
406 | Sleep without blocking from inside a function that was run via app:run().
407 | While the function is sleeping, other timers and events continue
408 | to be processed.
409 |
410 | This is poor man's multi-threading based on timers and coroutines.
411 | It can be used to create complex temporal sequences withoug having to chain
412 | timer callbacks.
413 |
414 | Calling sleep() outside an app:run() function raises an error.
415 |
416 | ## Window tracking
417 |
418 | ### `app:windows([filter]) -> {win1, ...}` `app:windows('#'[, filter]) -> n`
419 |
420 | Get all windows in creation order. If '#' is given, get the number of windows
421 | (dead or alive) instead. An optional `filter(self, win) -> false` function
422 | can be used to filter the results in both cases.
423 |
424 | ### `app:window_created(win)`
425 |
426 | Event: a window was created.
427 |
428 | ### `app:window_closed(win)`
429 |
430 | Event: a window was closed.
431 |
432 | ## Creating windows
433 |
434 | ### `app:window(t) -> win` `app:window(cw, ch, [title], [visible]) -> win`
435 |
436 | Create a window (fields of _`t`_ below with default value in parenthesis):
437 |
438 | * __position__
439 | * `x`, `y` - frame position
440 | * `w`, `h` - frame size
441 | * `cx`, `cy` - client area position
442 | * `cw`, `ch` - client area size
443 | * `min_cw`, `min_ch` - min client rect size (`1, 1`)
444 | * `max_cw`, `max_ch` - max client rect size
445 | * __state__
446 | * `visible` - start visible (`true`)
447 | * `minimized` - start minimized (`false`)
448 | * `maximized` - start maximized (`false`)
449 | * `enabled` - start enabled (true)
450 | * __frame__
451 | * `frame` - frame type: `'normal'`, `'none'`, `'toolbox'` (`'normal'`)
452 | * `title` - title (`''`)
453 | * `transparent` - transparent window (`false`)
454 | * `corner_radius` - rounded corners (`0`)
455 | * __behavior__
456 | * `parent` - parent window
457 | * `sticky` - moves with parent (`false`)
458 | * `topmost` - stays on top of other non-topmost windows (`false`)
459 | * `minimizable` - allow minimization (`true`)
460 | * `maximizable` - allow maximization (`true`; `false` if `resizeable` is `false`)
461 | * `closeable` - allow closing (`true`)
462 | * `resizeable` - allow resizing (`true`)
463 | * `fullscreenable` - allow fullscreen mode (`true`; `false` if `resizeable` is `false`)
464 | * `activable` - allow activation (`true`)
465 | * `autoquit` - quit the app on closing (`false`)
466 | * `hideonclose` - hide on close instead of destroying (`true`)
467 | * `edgesnapping` - magnetized edges (`'screen'`)
468 | * __rendering__
469 | * `opengl` - enable and [configure OpenGL](#winviewgl---gl) on the window
470 | * __menu__
471 | * `menu` - the menu bar
472 | * __tooltip__
473 | * `tooltip` - tooltip text (`false`)
474 |
475 | ### Initial size and position
476 |
477 | You can pass any combination of `x`, `y`, `w`, `h`, `cx`, `cy`, `cw`, `ch`
478 | as long as you pass both the width and the height in one way or another.
479 | The position is optional and it defaults to OS-driven cascading.
480 |
481 | Additionally, `x` and/or `y` can be `'center-main'` or `'center-active'`
482 | which will center the window on the main or active display respectively.
483 |
484 | If the size is max-constrained by either `max_cw`, `max_ch`
485 | or `resizeable = false` then `maximizable = false` and
486 | `fullscreenable = false` must also be set.
487 |
488 | Expect the OS to adjust the window size and/or position in unspecified
489 | ways for off-screen windows, windows too small to fit all titlebar buttons,
490 | windows with zero or negative client size or windows that are very large.
491 | Some adjustments are delayed to when the window is shown.
492 |
493 | ### The window state
494 |
495 | The window state is the combination of multiple flags (`minimized`,
496 | `maximized`, `fullscreen`, `visible`, `active`) plus its position, size
497 | and frame in current state (`client_rect` and `frame_rect`), and in normal
498 | state (`normal_frame_rect`).
499 |
500 | State flags are independent of each other, so they can be in almost
501 | any combination at the same time. For example, a window which starts
502 | with `{visible = false, minimized = true, maximized = true}`
503 | is initially hidden. If later made visible with `win:show()`,
504 | it will show minimized. If the user then unminimizes it, it will restore
505 | to maximized state. Throughout all these stages the `maximized` flag
506 | is `true`.
507 |
508 | ### Coordinate systems
509 |
510 | * window-relative positions are relative to the top-left corner of the window's client area.
511 | * screen-relative positions are relative to the top-left corner of the main screen.
512 |
513 | ## Child windows
514 |
515 | Child windows (`parent = win`) are top-level windows (so framed, not clipped)
516 | that stay on top of their parent, minimize along with their parent,
517 | and don't appear in the taskbar.
518 |
519 | The following defaults are different for child windows:
520 |
521 | * `minimizable`: false (must be false)
522 | * `maximizable`: false
523 | * `fullscreenable`: false
524 | * `edgesnapping`: 'parent siblings screen'
525 | * `sticky`: true
526 |
527 | Child windows can't be minimizable because they don't appear in the taskbar
528 | (they minimize when their parent is minimized). Child windows remain
529 | visible if their parent is hidden (or is created hidden).
530 |
531 | ### `win:parent() -> win|nil`
532 |
533 | Get the window's parent (read-only).
534 |
535 | ### `win:children() -> {win1, ...}`
536 |
537 | Get the window's children (those whose parent() is this window).
538 |
539 | ### Sticky windows
540 |
541 | Sticky windows (`sticky = true`) follow their parent when their parent is moved.
542 |
543 | __NOTE:__ Sticky windows [don't work](https://github.com/luapower/nw/issues/27) on Linux.
544 |
545 | ### `win:sticky() -> t|f`
546 |
547 | Get the sticky flag (read-only).
548 |
549 | ### Toolbox windows
550 |
551 | Toolbox windows (`frame = 'toolbox'`) show a thin title bar on Windows
552 | (they show a normal frame on OSX and Linux). They must have a parent.
553 |
554 | ## Transparent windows
555 |
556 | Transparent windows (`transparent = true`) allow using the full alpha channel
557 | when drawing on them. They also come with serious limitations (mostly from Windows):
558 |
559 | * they can't be framed so you must pass `frame = 'none'`.
560 | * they can't have views.
561 | * you can't draw on them using OpenGL.
562 |
563 | Despite these limitations, transparent windows are the only way to create
564 | free-floating tooltips and custom-shaped notification windows.
565 |
566 | ### `win:transparent() -> t|f`
567 |
568 | Get the transparent flag (read-only).
569 |
570 | ## Window closing
571 |
572 | Closing the window hides it or destroys it depending on the `hideonclose` flag.
573 | You can prevent closing by returning `false` in the `win:closing()` event.
574 |
575 | ### `win:close([force])`
576 |
577 | Close the window. Children are closed first. The `force` arg allows closing
578 | the window without firing the `win:closing()` event.
579 |
580 | Calling `close()` on a closed window does nothing.
581 |
582 | Closing a window results in hiding it or freeing it, depending on the
583 | `hideonclose` flag.
584 |
585 | ### `win:dead() -> t|f`
586 |
587 | Check if the window was destroyed.
588 | Calling any other method on a dead window raises an error.
589 |
590 | ### `win:closing(reason, [closing_win])`
591 |
592 | Event: The window is about to close. Reason can be `'quit'`, `'close'`,
593 | or the first argument passed to `close()`.
594 | When reason is `'close'`, `closing_win` is the window initiating the process.
595 | Return `false` from the event handler to refuse closing.
596 |
597 | ### `win:closed()`
598 |
599 | Event: The window was closed and is about to be destroyed.
600 | Fired after all children are closed, but before the window itself is destroyed.
601 | This event does not fire when `hideonclose` is `true` and the window is
602 | closed by the user or by calling `close()` (check the `hidden` event then).
603 |
604 | ### `win:closeable() -> t|f`
605 |
606 | Get the closeable flag (read-only).
607 |
608 | ### `win:hideonclose(t|f) /-> t|f`
609 |
610 | What to do when a window is closed: hide it or destroying it.
611 |
612 | ### `win:free([force])`
613 |
614 | Close and destroy the window (same as `close()` when `hideonclose` is set
615 | to `false`).
616 |
617 | __NOTE:__ Ensure that all the windows are freed before the process exits
618 | (which is why the `autoquit` option calls `free()` on the windows instead
619 | of `close()` which might just hide them). Don't leave it to the gc to free
620 | window objects because a window object contains other gc'ed objects that
621 | need to be freed in a specific order but the order in which `ffi.gc`
622 | destructors are called is undefined when the window object is gc'ed.
623 |
624 | ## Window & app activation
625 |
626 | Activation is about app activation and window activation. Activating a
627 | window programatically has an immediate effect only while the app is active.
628 | If the app is inactive, the window is not activated until the app becomes
629 | active and the user is notified in some other less intrusive way.
630 |
631 | If the user activates a different app in the interval between app launch
632 | and first window being shown, the app won't be activated back (this is a good
633 | thing usability-wise). This doesn't work on Linux (new windows always pop
634 | in your face because there's no concept of an "app" really in X).
635 |
636 | ### `app:active() -> t|f`
637 |
638 | Check if the app is active.
639 |
640 | ### `app:activate([mode])`
641 |
642 | Activate the app, which activates the last window that was active
643 | before the app got deactivated.
644 |
645 | The _mode_ arg can be:
646 |
647 | * 'alert' (default; Windows and OSX only; on Linux it does nothing)
648 | * 'force' (OSX and Linux only; on Windows it's the same as 'alert')
649 | * 'info' (OSX only; on Windows it's the same as 'alert'; on Linux it does nothing)
650 |
651 | The 'alert' mode: on Windows, this flashes the window on the taskbar until
652 | the user activates the window. On OSX it bounces the dock icon until the user
653 | activates the app. On Linux it does nothing.
654 |
655 | The 'force' mode: on Windows this is the same as the 'alert' mode.
656 | On OSX and Linux it pops up the window in the user's face
657 | (very rude, don't do it).
658 |
659 | The 'info' mode: this special mode allows bouncing up the dock icon
660 | on OSX only once. On other platforms it's the same as the default 'alert' mode.
661 |
662 | ### `app:activated()` `app:deactivated()`
663 |
664 | Event: the app was activated/deactivated.
665 |
666 | ### `app:active_window() -> win|nil`
667 |
668 | Get the active window, if any (nil if the app is inactive).
669 |
670 | ### `win:active() -> t|f`
671 |
672 | Check if the window is active (`false` for all windows if the app is inactive).
673 |
674 | ### `win:activate()`
675 |
676 | Activate the window. If the app is inactive, this does _not_ activate the window.
677 | Instead it only marks the window to be activated when the app becomes active.
678 | If you want to alert the user that it should pay attention to the app/window,
679 | call `app:activate()` after calling this function.
680 |
681 | ### `win:activated()` `win:deactivated()`
682 |
683 | Event: window was activated/deactivated.
684 |
685 | ### `win:activable() -> t|f`
686 |
687 | Get the activable flag (read-only). This is useful for creating popup menus
688 | that can be clicked on without stealing keyboard focus away from the main
689 | window.
690 |
691 | __NOTE:__ Only works with frameless windows.
692 |
693 | __NOTE:__ This [doesn't work](https://github.com/luapower/nw/issues/26) in Linux.
694 |
695 | ## App instances
696 |
697 | ### `app:check_single_instance()`
698 |
699 | If another instance of this app is already running, activate it and exit
700 | this process. Calling this at the beginning of the app (after setting
701 | `nw.app_id` if that's necessasry) is enough to enable single-app instance
702 | behavior.
703 |
704 | ### `nw.app_id = id`
705 |
706 | Set the app ID for single-app-instance checks. All processes with the same
707 | app ID will be considered instances of the same app. If this is not set,
708 | the executable file which started the process is used as app ID.
709 |
710 | __NOTE:__ This must be set before calling `nw:app()` for the first time.
711 |
712 | ### `app:already_running() -> t|f`
713 |
714 | Check if other instances of this app are running.
715 |
716 | ### `app:wakeup_other_instances()`
717 |
718 | Send `wakeup` event to other instances of this app.
719 |
720 | ### `app:wakeup()`
721 |
722 | Event: another instance of this app has called `app:wakeup_other_instances()`.
723 |
724 | ## App visibility (OSX)
725 |
726 | ### `app:visible() -> t|f` `app:visible(t|f)` `app:hide()` `app:unhide()`
727 |
728 | Get/set app visibility.
729 |
730 | ### `app:hidden()` `app:unhidden()`
731 |
732 | Event: app was hidden/unhidden.
733 |
734 | ## Window state
735 |
736 | ### `win:show()`
737 |
738 | Show the window in its previous state (which can include any combination
739 | of minimized, maximized, and fullscreen state flags).
740 |
741 | When a hidden window is shown it is also activated, except if it was
742 | previously minimized, in which case it is shown in minimized state
743 | without being activated.
744 |
745 | Calling show() on a visible (which includes minimized) window does nothing.
746 |
747 | ### `win:hide()`
748 |
749 | Hide the window from the screen and from the taskbar, preserving its full state.
750 |
751 | Calling hide() on a hidden window does nothing.
752 |
753 | ### `win:visible() -> t|f`
754 |
755 | Check if a window is visible (note: that includes minimized).
756 |
757 | ### `win:visible(t|f)`
758 |
759 | Calls `show()` or `hide()` to change the window's visibility.
760 |
761 | ### `win:shown()` `win:hidden()`
762 |
763 | Event: window was shown/hidden.
764 |
765 | ### `win:minimizable() -> t|f`
766 |
767 | Get the minimizable flag (read-only).
768 |
769 | ### `win:isminimized() -> t|f`
770 |
771 | Get the minimized state. This flag remains `true` when a minimized window is hidden.
772 |
773 | ### `win:minimize()`
774 |
775 | Minimize the window and deactivate it. If the window is hidden,
776 | it is shown in minimized state (and the taskbar button is not activated).
777 |
778 | ### `win:minimized()` `win:unminimized()`
779 |
780 | Event: window was minimized/unminimized.
781 |
782 | ### `win:maximizable() -> t|f`
783 |
784 | Get the maximizable flag (read-only).
785 |
786 | ### `win:ismaximized() -> t|f`
787 |
788 | Get the maximized state. This flag stays `true` if a maximized window
789 | is minimized, hidden or enters fullscreen mode.
790 |
791 | ### `win:maximize()`
792 |
793 | Maximize the window and activate it. If the window was hidden,
794 | it is shown in maximized state and activated.
795 |
796 | If the window is already maximized it is not activated.
797 |
798 | ### `win:maximized()` `win:unmaximized()`
799 |
800 | Event: window was maximized/unmaximized.
801 |
802 | ### `win:fullscreenable() -> t|f`
803 |
804 | Check if a window is allowed to go in fullscreen mode (read-only).
805 | This flag only affects OSX - the only platform which presents a fullscreen
806 | button on the title bar. Fullscreen mode can always be engaged programatically.
807 |
808 | ### `win:fullscreen() -> t|f`
809 |
810 | Get the fullscreen state.
811 |
812 | ### `win:fullscreen(t|f)`
813 |
814 | Enter or exit fullscreen mode and activate the window. If the window is hidden
815 | or minimized, it is shown in fullscreen mode and activated.
816 |
817 | If the window is already in the desired mode it is not activated.
818 |
819 | ### `win:entered_fullscreen()` `win:exited_fullscreen()`
820 |
821 | Event: entered/exited fullscreen mode.
822 |
823 | ### `win:restore()`
824 |
825 | Restore from minimized, maximized or fullscreen state, i.e. unminimize
826 | if the window was minimized, exit fullscreen if it was in fullscreen mode,
827 | or unmaximize it if it was maximized (otherwise do nothing).
828 |
829 | The window is always activated unless it was in normal mode.
830 |
831 | ### `win:shownormal()`
832 |
833 | Show the window in normal state.
834 |
835 | The window is always activated even when it's already in normal mode.
836 |
837 | State tracking is about getting and tracking the entire user-changeable
838 | state of a window (of or the app) as a whole.
839 |
840 | ### `win:showmodal()`
841 |
842 | Show as modal window to its parent. A modal window disables its parent while
843 | it is visible and enables it back when it gets hidden again. The window must
844 | be activable and must have a parent or an error is raised.
845 |
846 | ### `win:changed(old_state, new_state)`
847 |
848 | Event: window user-changeable state (i.e. any of the `visible`, `minimized`,
849 | `maximized`, `fullscreen` or `active` flags) has changed.
850 |
851 | ### `app:changed(old_state, new_state)`
852 |
853 | Event: app user-changeable state (i.e. the `visible` or `active` flag) has
854 | changed.
855 |
856 | ### `win:enabled() -> t|f` `win:enabled(t|f)`
857 |
858 | Get/set the enabled flag (default: true). A disabled window cannot receive
859 | mouse or keyboard focus. Disabled windows are useful for implementing
860 | modal windows: make a child window and disable the parent while showing
861 | the child, and enable back the parent when closing the child.
862 |
863 | __NOTE:__ This [doesn't work](https://github.com/luapower/nw/issues/25) on Linux.
864 |
865 | ## Frame extents
866 |
867 | ### `app:frame_extents(frame, has_menu, resizeable) -> left, top, right, bottom`
868 |
869 | Get the frame extents for a certain frame type.
870 | If `has_menu` is `true`, then the window also has a menu.
871 |
872 | ### `app:client_to_frame(frame, has_menu, resizeable, x, y, w, h) -> x, y, w, h`
873 |
874 | Given a client rectangle, return the frame rectangle for a certain
875 | frame type. If `has_menu` is `true`, then the window also has a menu.
876 |
877 | ### `app:frame_to_client(frame, has_menu, resizeable, x, y, w, h) -> x, y, w, h`
878 |
879 | Given a frame rectangle, return the client rectangle for a certain
880 | frame type. If `has_menu` is `true`, then the window also has a menu.
881 |
882 | ## Size and position
883 |
884 | ### `win:client_rect() -> cx, cy, cw, ch` `win:client_rect(cx, cy, cw, ch)` `win:frame_rect() -> x, y, w, h` `win:frame_rect(x, y, w, h)` `win:client_size() -> cw, ch` `win:client_size(cw, ch)`
885 |
886 | Get/set the client/frame rect/size in screen coordinates.
887 |
888 | When getting: returns nothing if the window is minimized.
889 |
890 | When setting: if any of the arguments is nil or `false`, it is replaced with
891 | the current value of that argument to allow for partial changes. Does nothing
892 | if the window is minimized, maximized, or in fullscreen mode.
893 |
894 | ### `win/view:to_screen(x, y) -> x, y` `win/view:to_client(x, y) -> x, y`
895 |
896 | Convert a point from client space to screen space and viceversa
897 | based on client_rect().
898 |
899 | ### `win:normal_frame_rect() -> x, y, w, h`
900 |
901 | Get the frame rect in normal state (in screen coordinates).
902 | Unlinke client_rect() and frame_rect(), this always returns a rectangle.
903 | This is useful for recreating a window in its previous state which
904 | includes the normal frame rectangle, the maximized flag, and optionally
905 | the minimized flag. It doesn't include the fullscreen flag
906 | (you cannot create a window in fullscreen mode but you can enter fullscreen
907 | mode afterwards).
908 |
909 | ### `win:sizing(when, how, rect) -> true|nil`
910 |
911 | Event: window size/position is about to change. The `rect` arg is a table
912 | with the fields _x, y, w, h_. Change these values in the table to affect
913 | the window's final size and position.
914 |
915 | __NOTE:__ This event does not fire in Linux.
916 |
917 | ### `win:client_rect_changed(cx, cy, cw, ch, oldcx, oldcy, oldcw, oldch)` `win:client_moved(cx, cy, oldcx, oldcy)` `win:client_resized(cw, ch, oldcw, oldch)` `win:frame_rect_changed(x, y, w, h, oldx, oldy, oldw, oldh)` `win:frame_moved(x, y, oldx, oldy)` `win:frame_resized(w, h, oldw, oldh)`
918 |
919 | Event: window was moved/resized. These events also fire when a window is
920 | hidden or minimized in which case all args are nil, so make sure to test for that.
921 |
922 | These events fire together every time in the same order:
923 |
924 | * `client_rect_changed`
925 | * `client_moved`
926 | * `client_resized`
927 | * `frame_rect_changed`
928 | * `frame_moved`
929 | * `frame_resized`
930 |
931 | ### `win:hittest(x, y, where) -> where`
932 |
933 | Hit test for moving and resizing frameless windows. Return 'left', 'top',
934 | 'right', 'bottom', 'topleft', 'bottomright', 'topright' or 'bottomleft'
935 | to specify that the window should be resized, 'move' which means the window
936 | should be moved, `false` which means the coordinates are over the client area,
937 | or nil which means that standard resizing should take place. The `where`
938 | arg is the default response for the given coordinates.
939 |
940 | ## Size constraints
941 |
942 | ### `win:resizeable() -> t|f`
943 |
944 | Check if the window is resizeable.
945 |
946 | ### `win:minsize() -> cw, ch` `win:minsize(cw, ch)` `win:minsize(false)`
947 |
948 | Get/set/clear the minimum client rect size.
949 |
950 | The constraint can be applied to one dimension only by passing `false` or nil
951 | for the other dimension. The window is resized if it was smaller than this size.
952 | The size is clamped to maxsize if that is set. The size is finally clamped to
953 | the minimum (1, 1) which is also the default.
954 |
955 | ### `win:maxsize() -> cw, ch` `win:maxsize(cw, ch)` `win:maxsize(false)`
956 |
957 | Get/set/clear the maximum client rect size.
958 |
959 | The constraint can be applied to one dimension only by passing `false` or nil
960 | for the other dimension. The window is resized if it was larger than this size.
961 | The size is clamped to minsize if that is set. Trying to set this on a
962 | maximizable or fullscreenable window raises an error.
963 |
964 | ## Edge snapping
965 |
966 | ### `win:edgesnapping() -> mode` `win:edgesnapping(mode)`
967 |
968 | Get/set edge snapping mode, which is a string containing any combination
969 | of the following words separated by spaces:
970 |
971 | * `'app'` - snap to app's windows
972 | * `'other'` - snap to other apps' windows
973 | * `'parent'` - snap to parent window
974 | * `'siblings'` - snap to sibling windows
975 | * `'screen'` or `true` - snap to screen edges
976 | * `'all'` - equivalent to 'app other screen'
977 | * `false` - disable snapping
978 |
979 | __NOTE:__ Edge snapping doesn't work on Linux because the `win:sizing()`
980 | event doesn't fire there. It is however already (poorly) implemented
981 | by some window managers (eg. Unity) so all is not lost.
982 |
983 | ### `win:magnets(which) -> {r1, ...}`
984 |
985 | Event: get edge snapping rectangles (rectangles are tables with fields _x, y, w, h_).
986 |
987 | ## Z-Order
988 |
989 | ### `win:topmost() -> t|f` `win:topmost(t|f)`
990 |
991 | Get/set the topmost flag. A topmost window stays on top of all other
992 | non-topmost windows.
993 |
994 | ### `win:raise([rel_to_win])`
995 |
996 | Raise above all windows/specific window.
997 |
998 | ### `win:lower([rel_to_win])`
999 |
1000 | Lower below all windows/specific window.
1001 |
1002 | ## Window title
1003 |
1004 | ### `win:title() -> title` `win:title(title)`
1005 |
1006 | Get/set the window's title.
1007 |
1008 | ## Displays
1009 |
1010 | In multi-monitor setups, the non-mirroring displays are mapped
1011 | on a virtual surface, with the main display's top-left corner at (0, 0).
1012 |
1013 | ### `app:displays() -> {disp1, ...}` `app:displays'#' -> n`
1014 |
1015 | Get displays (in no specific order). Mirroring displays are not included.
1016 | If '#' is given, get the display count instead.
1017 |
1018 | ### `app:main_display() -> disp`
1019 |
1020 | Get the display whose screen rect is at (0, 0).
1021 |
1022 | ### `app:active_display() -> disp`
1023 |
1024 | Get the display which contains the active window, falling back to the main
1025 | display if there is no active window.
1026 |
1027 | ### `disp:screen_rect() -> x, y, w, h` `disp.x, disp.y, disp.w, disp.h`
1028 |
1029 | Get the display's screen rectangle.
1030 |
1031 | ### `disp:desktop_rect() -> cx, cy, cw, ch` `disp.cx, disp.cy, disp.cw, disp.ch`
1032 |
1033 | Get the display's desktop rectangle (screen minus any taskbars).
1034 |
1035 | __NOTE:__ This doesn't work in Linux for secondary monitors (it gives the screen rect).
1036 |
1037 | ### `app:displays_changed()`
1038 |
1039 | Event: displays changed.
1040 |
1041 | ### `win:display() -> disp|nil`
1042 |
1043 | Get the display the window is currently on. Returns nil if the window
1044 | is off-screen. Returns the correct display based on the window's coordinates
1045 | even if the window is hidden.
1046 |
1047 | ## Cursors
1048 |
1049 | ### `win:cursor() -> name, t|f` `win:cursor(name|t|f)`
1050 |
1051 | Get/set the mouse cursor and/or visibility. The name can be:
1052 |
1053 | * 'arrow' (default)
1054 | * 'text'
1055 | * 'hand'
1056 | * 'cross'
1057 | * 'forbidden'
1058 | * 'size_diag1' (i.e. NE-SW, forward-slash-looking)
1059 | * 'size_diag2' (i.e. NW-SE, backslash-looking)
1060 | * 'size_h'
1061 | * 'size_v'
1062 | * 'move'
1063 | * 'busy_arrow'
1064 | * 'top', 'left', 'right', 'bottom', 'topleft', 'topright',
1065 | 'bottomleft', 'bottomright' (only different in Linux)
1066 |
1067 | ## Keyboard
1068 |
1069 | See [nw_keyboard] for the list of key names.
1070 |
1071 | ### `app:key(query) -> t|f`
1072 |
1073 | Get key pressed and toggle states. The query can be one or more
1074 | key names separated by spaces or by `+` eg. 'alt+f3' or 'alt f3'.
1075 |
1076 | The key name can start with `^` in which case the toggle state of that key
1077 | is queried instead eg. '^capslock' returns the toggle state of the caps lock
1078 | key while 'capslock' returns its pressed state. (only the capslock, numlock
1079 | and scrolllock keys have toggle states).
1080 |
1081 | The key name can start with `!` which checks that the key is _not_ pressed.
1082 |
1083 | ### `win:keydown(key)`
1084 |
1085 | Event: a key was pressed (not sent on repeat).
1086 |
1087 | ### `win:keyup(key)`
1088 |
1089 | Event: a key was depressed.
1090 |
1091 | ### `win:keypress(key)`
1092 |
1093 | Event: sent after keydown and on key repeat.
1094 |
1095 | ### `win:keychar(s)`
1096 |
1097 | Event: sent after keypress for displayable characters; _`s`_ is a utf-8
1098 | string and can contain one or more code points.
1099 |
1100 | ## Hi-DPI support
1101 |
1102 | By default, windows contents are scaled by the OS on Hi-DPI screens,
1103 | so they look blurry but they are readable even if the app is unaware
1104 | that it is showing on a dense screen. Making the app Hi-DPI-aware means
1105 | telling the OS to disable this automatic raster scaling and allow the
1106 | app to scale the UI itself (but this time in vector space) in order
1107 | to make it readable again on a dense screen.
1108 |
1109 | ### `app:autoscaling() -> t|f`
1110 |
1111 | Check if autoscaling is enabled.
1112 |
1113 | ### `app:autoscaling(t|f)`
1114 |
1115 | Enable/disable autoscaling.
1116 |
1117 | __NOTE:__ This function must be called before the OS stretcher kicks in,
1118 | i.e. before creating any windows or calling any display APIs.
1119 | It will silently fail otherwise.
1120 |
1121 | ### `disp.scalingfactor`
1122 |
1123 | The display's scaling factor is an attribute of display objects.
1124 | This is 1 when autoscaling is enabled and > 1 when disabled
1125 | and the display is hi-dpi.
1126 |
1127 | If autoscaling is disabled, windows must check their display's
1128 | scaling factor and scale the UI accordingly.
1129 |
1130 | ### `win:scalingfactor_changed()`
1131 |
1132 | A window's display scaling factor changed or most likely the window
1133 | was moved to a screen with a different scaling factor.
1134 |
1135 | ## Views
1136 |
1137 | A view object defines a rectangular region within a window for drawing
1138 | and receiving mouse events.
1139 |
1140 | Views allow partitioning a window's client area into multiple non-overlapping
1141 | regions that can be rendered using different technologies.
1142 | In particular, you can use OpenGL on some views, while using bitmaps
1143 | (and thus cairo) on others. This presents a simple solution to the problem
1144 | of drawing an antialiased 2D UI around a 3D scene as an alternative to
1145 | drawing on the textures of orto-projected quads. Views also allow placing
1146 | native widgets alongside custom-painted areas on the same window.
1147 |
1148 | __NOTE:__ If you use views, bind all mouse events to the views.
1149 | Do not mix window and view mouse events since the behavior of window
1150 | mouse events in the presence of views is
1151 | [not consistent](https://github.com/luapower/nw/issues/54)
1152 | between platforms.
1153 |
1154 | ### `win:views() -> {view1, ...}` `win:views'#' -> n`
1155 |
1156 | Get the window's views. If '#' is given, get the view count instead.
1157 |
1158 | ### `win:view(t) -> view`
1159 |
1160 | Create a view (fields of _`t`_ below):
1161 |
1162 | * `x`, `y`, `w`, `h` - view's position (in window's client space) and size
1163 | * `visible` - start visible (default: true)
1164 | * `anchors` - resizing anchors (default: 'lt'); can be 'ltrb'
1165 | * `opengl` - enable and [configure OpenGL](#winviewgl---gl) on the view.
1166 |
1167 | __NOTE:__ The width and height are clamped to the minimum (1, 1).
1168 |
1169 | ### `view:free()`
1170 |
1171 | Destroy the view.
1172 |
1173 | ### `view:dead() -> t|f`
1174 |
1175 | Check if the view was destroyed.
1176 |
1177 | ### `view:visible() -> t|f` `view:visible(t|f)` `view:show()` `view:hide()`
1178 |
1179 | Get/set the view's visibility.
1180 |
1181 | The position and size of the view are preserved while hidden (anchors keep working).
1182 |
1183 | ### `view:rect() -> x, y, w, h` `view:rect(x, y, w, h)`
1184 |
1185 | Get/set the view's position (in window's client space) and size.
1186 |
1187 | The view rect is valid and can be changed while the view is hidden.
1188 |
1189 | ### `view:size() -> w, h` `view:size(w, h)`
1190 |
1191 | Get/set the view's size.
1192 |
1193 | ### `view:anchors() -> anchors` `view:anchors(anchors)`
1194 |
1195 | Get/set the anchors: they can be any combination of 'ltrb' characters
1196 | representing left, top, right and bottom anchors respectively.
1197 |
1198 | Anchors are a simple but effective way of doing stitched layouting.
1199 | This is how they work: there's four possible anchors which you can set,
1200 | one for each side of the view. Setting an anchor on one side fixates
1201 | the distance between that side and the same side of the window
1202 | the view is on, so that when the window is moved/resized, the view
1203 | is also moved/resized in order to preserve the initial distance
1204 | to that side of the window.
1205 |
1206 | ### `view:rect_changed(x, y, w, h)` `view:moved(x, y)` `view:resized(w, h)`
1207 |
1208 | Event: view's size and/or position changed.
1209 |
1210 | ## Mouse
1211 |
1212 | ### `win/view:mouse(var) -> val`
1213 |
1214 | Get the mouse state. The `var` arg can be:
1215 | 'x', 'y', 'pos', 'inside', 'left', 'right', 'middle', 'x1', 'x2'.
1216 |
1217 | The mouse state is not queried: it is the state at the time of the last
1218 | mouse event. Returns nothing if the window is hidden or minimized.
1219 |
1220 | Mouse coordinates are relative to the window's client-area.
1221 |
1222 | ### `win/view:mouseenter()` `win/view:mouseleave()`
1223 |
1224 | Event: mouse entered/left the client area of the window.
1225 |
1226 | These events do not fire while the mouse is captured (see mousedown)
1227 | but a mouseleave event _will_ fire after mouseup _if_ mouseup happens
1228 | outside the client area of the window/view that captured the mouse.
1229 |
1230 | ### `win/view:mousemove(x, y)`
1231 |
1232 | Event: the mouse was moved.
1233 |
1234 | ### `win/view:mousedown(button, x, y, click_count)`
1235 |
1236 | Event: a mouse button was pressed; button can be 'left', 'right', 'middle', 'x1', 'x2'.
1237 |
1238 | While a mouse button is down, the mouse is _captured_ by the window/view
1239 | which received the mousedown event, which means that the same window/view
1240 | will continue to receive mousemove events even if the mouse leaves
1241 | its client area.
1242 |
1243 | ### `win/view:mouseup(button, x, y, click_count)`
1244 |
1245 | Event: a mouse button was depressed.
1246 |
1247 | ### `win/view:click(button, count, x, y)`
1248 |
1249 | Event: a mouse button was clicked (fires immediately after mousedown).
1250 |
1251 | ### Repeated clicks
1252 |
1253 | #### TL;DR
1254 |
1255 | ~~~{.lua}
1256 | function win:click(button, count, x, y)
1257 | if count == 2 then --double click
1258 | ...
1259 | elseif count == 3 then --triple click
1260 | ...
1261 | return true --triple click is as high as we go in this app
1262 | end
1263 | end
1264 | ~~~
1265 |
1266 | #### How it works
1267 |
1268 | When the user clicks the mouse repeatedly, with a small enough interval
1269 | between clicks and over the same target, a counter is incremented.
1270 | When the interval between two clicks is larger than the threshold
1271 | or the mouse is moved too far away from the initial target,
1272 | the counter is reset (i.e. the click-chain is interrupted).
1273 | Returning `true` on the `click()` event also resets the counter.
1274 |
1275 | This allows processing of double-clicks, triple-clicks, or multi-clicks
1276 | by checking the `count` argument on the `click()` event. If your app
1277 | doesn't need to process double-clicks or multi-clicks, you can just ignore
1278 | the `count` argument. If it does, you must return `true` after processing
1279 | the event with the highest count so that the counter is reset.
1280 |
1281 | For instance, if your app supports double-click over some target,
1282 | you must return `true` when count is 2, otherwise you might get a count of 3
1283 | on the next click sometimes, instead of 1 as expected. If your app
1284 | supports both double-click and triple-click over a target,
1285 | you must return `true` when the count is 3 to break the click chain,
1286 | but you must not return anything when the count is 2,
1287 | or you'll never get a count of 3.
1288 |
1289 | The double-click time interval is from the user's mouse settings
1290 | and it is queried on every click.
1291 |
1292 | ### `win/view:mousewheel(delta, x, y, pixeldelta)` `win/view:mousehwheel(delta, x, y, pixeldelta)`
1293 |
1294 | Event: the mouse vertical or horizontal wheel was moved.
1295 | The delta represents the number of lines to scroll.
1296 |
1297 | The number of lines per scroll notch is from the user's mouse settings
1298 | and it is queried on every wheel event (Windows, OSX).
1299 |
1300 | The extra `pixeldelta` arg is given on OSX on devices where analog scrolling
1301 | is available, in which case that value should be used instead.
1302 |
1303 | ## Rendering
1304 |
1305 | Drawing on a window or view must be done inside the `repaint()` event
1306 | by requesting the window/view's bitmap or OpenGL context and drawing on it.
1307 | The OS fires `repaint` whenever it loses (part of) the contents
1308 | of the window. To force a repaint anytime, use `win:invalidate()`.
1309 |
1310 | __NOTE:__ You can't request a bitmap on an OpenGL-enabled window/view
1311 | and you can't request an OpenGL context on a non-OpenGL-enabled window/view.
1312 | To enable OpenGL on a window/view you must pass an `opengl` options table
1313 | to the window/view creation function (it can be an empty table or just `true`).
1314 |
1315 | ### `win/view:repaint()`
1316 |
1317 | Event: window needs repainting. To repaint the window, simply request
1318 | the window's bitmap or OpenGL context and draw using that.
1319 |
1320 | ### `win/view:invalidate([invalid_clock])`
1321 |
1322 | Request sync'ing and repainting. The optional `invalid_clock` (which defaults
1323 | to `-inf`) specifies the earliest `time.clock()` when the window/view should
1324 | be repainted (this is useful for implementing delayed animations efficiently).
1325 |
1326 | ### `win/view:invalid([at_clock]) -> t|f`
1327 |
1328 | Check if the window/view is invalid at a specific time point (which defaults
1329 | to `time.clock()`).
1330 |
1331 | ### `win/view:validate([at_clock])`
1332 |
1333 | Fire the `sync()` event if the window/view is invalid.
1334 |
1335 | ### `win:sync()`
1336 |
1337 | Event: window needs sync'ing. This event is fired before `repaint()`,
1338 | but only as a result of calling `invalidate()`.
1339 |
1340 | The point of this function is to separate updating the logical representation
1341 | of a window or view (i.e. its layout) from updating its raster representation
1342 | (i.e. its pixels), so that in some parts of the code you can signal that the
1343 | layout was put in an inconsistent state and must be sync'ed on the next frame,
1344 | while in other parts of the code you can ask that the layout be sync'ed
1345 | immediately (eg. because you need to hit-test it on a `mousemove` event),
1346 | and all this can happen between frames, independent of the repainting cycle.
1347 |
1348 | ### `win/view:bitmap() -> bmp`
1349 |
1350 | Get a bgra8 [bitmap] object to draw on. The bitmap is freed and replaced when
1351 | the window's client area changes size. The bitmap must be requested inside
1352 | the `repaint()` event for drawing purposes, but can also be requested outside
1353 | the `repaint()` event for hit-testing purposes.
1354 |
1355 | The alpha channel is not used unless this is a transparent window
1356 | (note: views cannot be transparent).
1357 |
1358 | ### `bmp:clear()`
1359 |
1360 | Fill the bitmap with zeroes.
1361 |
1362 | ### `bmp:cairo() -> cr`
1363 |
1364 | Get a [cairo] context on the bitmap. The context lasts as long as the bitmap lasts.
1365 |
1366 | ### `win/view:free_cairo(cr)`
1367 |
1368 | Event: cairo context needs to be freed.
1369 |
1370 | ### `win/view:free_bitmap(bmp)`
1371 |
1372 | Event: bitmap needs to be freed.
1373 |
1374 | ### `win/view:gl() -> gl`
1375 |
1376 | Get an OpenGL context/API to draw on the window or view. For this to work
1377 | OpenGL must be enabled on the window or view via the `opengl` options table,
1378 | which can have the fields:
1379 |
1380 | * `profile` - OpenGL profile to use: '1.0', '3.2' ('1.0')
1381 | * `antialiasing` - enable antialiasing: 'supersample', 'multisample', true, false (false)
1382 | * `samples` - number of samples for 'multisample' antialiasting (4)
1383 | * `vsync` - vertical sync: true, false, swap-interval (true)
1384 |
1385 | ## Menus
1386 |
1387 | ### `app:menu() -> menu`
1388 |
1389 | Create a menu.
1390 |
1391 | ### `app:menubar() -> menu`
1392 |
1393 | Get the app's menu bar (OSX)
1394 |
1395 | ### `win:menubar() -> menu|nil` `win:menubar(menu|nil)`
1396 |
1397 | Get/set/remove the window's menu bar (Windows, Linux).
1398 |
1399 | ### `win/view:popup(menu, cx, cy)` `menu:popup(win/view, cx, cy)`
1400 |
1401 | Pop up a menu at a point relative to a window or view.
1402 |
1403 | ### `menu:add([index, ]text, [action], [options])` `menu:set(index, text, [action], [options])` `menu:add{index =, text =, action =,