├── 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 =,