1-- 2-- pseudo-window manager helper script for working with wayland and 3-- x-wayland clients specifically. 4-- 5-- Returns a factory function for taking a wayland-bridge carrying a 6-- single client and constructing a wm/state tracker - as well as an 7-- optional connection manager that takes any primary connection that 8-- identifies as 'bridge-wayland' and deals with both 1:1 and 1:* 9-- cases. 10-- 11-- local factory, connection = system_load("builtin/wayland.lua")() 12-- factory(vid, segreq_table, config) 13-- 14-- (manual mode from a handler to an established bridge-wayland): 15-- if status.kind == "segment-request" then 16-- wm = factory(vid, status, config) 17-- end 18-- 19-- (connection mode): 20-- local vid = target_alloc("wayland_only", function() end) 21-- 22-- connection(vid, 23-- function(source, status) 24-- wm = factory(source, status, config) 25-- end) 26-- 27-- a full example can be found in tests/interactive/wltest 28-- 29-- config contains display options and possible wm event hooks for 30-- latching into an outer wm scheme. 31-- 32-- possible methods in config table: 33-- 34-- decorate(window, vid, width, height) => t, l, d, r: 35-- attach / set decorations using vid as anchor and width/height. 36-- called whenever decoration state/sizes change. If 'vid' is not 37-- provided, it means existing decorations should be destroyed. 38-- 39-- the decorator should return the number of pixels added in each 40-- direction for maximize/fullscreen calculations to be correct. 41-- 42-- destroy(window): 43-- called when a window has been destroyed, just before video 44-- resources are deleted. 45-- 46-- focus(window) or focus(): 47-- called when a window requests focus, to acknowledge, call 48-- wnd:focus() and wnd:unfocus() respectively. 49-- 50-- move(window, x, y, dx, dy): 51-- called when a window requests to be moved to x, y, return 52-- x, y if permitted - or constrain coordinates and return new 53-- position 54-- 55-- configure(window, [type]): 56-- request for an initial size for a toplevel window (if any) 57-- should return w, h, x, y 58-- 59-- state_change(window, state={ 60-- "fullscreen", "realized", "composed", 61-- "maximized", "visible", "focused", "toplevel"} 62-- , [popup|parent], [grab]) 63-- 64-- mapped(window): 65-- called when a new toplevel window is ready to be drawn 66-- 67-- log : function(domain, message) (default: print) 68-- fmt : function(string, va_args) (default: string.format) 69-- 70-- the 'window' table passed as arguments provides the following methods: 71-- 72-- focus(self): 73-- acknowledge a focus request, raise and change pending visuals 74-- 75-- unfocus(self): 76-- mark the window has having lost focus 77-- 78-- maximize(self): 79-- acknowledge a maximization request 80-- 81-- minimize(self): 82-- acknowledge a minimization request 83-- 84-- fullscreen(self): 85-- acknowledge a fullscreen request or force fullscreen 86-- 87-- destroy(self): 88-- kill window and associated resources 89-- 90-- the 'window' table passed as arguments provides the following properties: 91-- mouse_proxy (vid) set if another object should be used to determine ownership 92-- 93-- the returned 'wm' table exposes the following methods: 94-- 95-- destroy(): 96-- drop all resources tied to this client 97-- 98-- resize(width, height, density): 99-- change the 'output' for this client, roughly equivalent to outmost 100-- window resizing, changing display or display resolution - density 101-- in ppcm. 102-- 103 104-- for drag and drop purposes, we need to track all bridges so that we can 105-- react on 'drag' and then check underlying vid when cursor-tagged in drag state, 106-- and then test and forward to clients beneath it 107local bridges = {} 108 109local x11_lut = 110{ 111 ["type"] = 112 function(ctx, source, typename) 113 ctx.states.typed = typename 114 if not ctx.states.mapped then 115 ctx:apply_type() 116 end 117 end, 118 ["pair"] = 119 function(ctx, source, wl_id, x11_id) 120 local wl_id = wl_id and wl_id or "missing" 121 local x11_id = x11_id and x11_id or "missing" 122 123 ctx.idstr = wl_id .. "-> " .. x11_id 124 ctx.wm.log("wl_x11", ctx.wm.fmt("paired:wl=%s:x11=%s", wl_id, x11_id)) 125-- not much to do with the information 126 end, 127 ["fullscreen"] = 128 function(ctx, source, on) 129 ctx.wm.state_change(ctx, "fullscreen") 130 end, 131} 132 133-- this table contains hacks around some bits in wayland that does not map to 134-- regular events in shmif and comes packed in 'message' events 135local wl_top_lut = 136{ 137 ["move"] = 138 function(ctx) 139 ctx.states.moving = true 140 end, 141 ["maximize"] = 142 function(ctx) 143 ctx.wm.state_change(ctx, "maximize") 144 end, 145 ["demaximize"] = 146 function(ctx) 147 ctx.wm.state_change(ctx, "demaximize") 148 end, 149 ["menu"] = 150 function(ctx) 151 ctx.wm.context_menu(ctx) 152 end, 153 ["fullscreen"] = 154 function(ctx, source, on) 155 ctx.wm.state_change(ctx, "fullscreen") 156 end, 157 ["resize"] = 158 function(ctx, source, dx, dy) 159 if not dx or not dy then 160 return 161 end 162 163 dx = tonumber(dx) 164 dy = tonumber(dy) 165 166 if not dx or not dy then 167 ctx.states.resizing = false 168 return 169 end 170 171-- masks for moving, used on left,top 172 local mx = 0 173 local my = 0 174 if dx < 0 then 175 mx = 1 176 end 177 if dy < 0 then 178 my = 1 179 end 180 181 ctx.states.resizing = {dx, dy, mx, my} 182 end, 183-- practically speaking there is really only xdg now, though if someone 184-- adds more, any wm specific mapping should be added here 185 ["shell"] = 186 function(ctx, shell_type) 187 end, 188 ["scale"] = 189 function(ctx, sf) 190 end, 191-- new window geometry 192 ["geom"] = 193 function(ctx, x, y, w, h) 194 ctx.wm.log("toplevel", ctx.wm.fmt("anchor_geom:x=%f:y=%f:w=%f:h=%f", x, y, w, h)) 195 ctx.anchor_offset = {x, y} 196 end 197} 198 199local function wnd_input_table(wnd, iotbl) 200 if not wnd.states.focused then 201 return 202 end 203 204 target_input(wnd.vid, iotbl) 205end 206 207-- these just request the focus state to change, the wm has final say 208local function wnd_mouse_over(wnd) 209 if wnd.wm.cfg.mouse_focus and not wnd.states.focused then 210 wnd.wm.focus(wnd) 211 end 212end 213 214-- this is not sufficient when we have a popup grab surface as 'out' 215-- may mean in on the popup 216local function wnd_mouse_out(wnd) 217 if wnd.wm.cfg.mouse_focus and wnd.states.focused then 218 wnd.wm.focus() 219 end 220end 221 222local function wnd_mouse_btn(wnd, vid, button, active, x, y) 223 if not active then 224 wnd.states.moving = false 225 wnd.states.resizing = false 226 227 elseif not wnd.states.focused then 228 wnd.wm.focus(wnd) 229 end 230 231-- catch any popups, this will cause spurious 'release' events in 232-- clients if the button mask isn't accounted for 233 if wnd.dismiss_chain then 234 if active then 235 wnd.block_release = button 236 wnd:dismiss_chain() 237 end 238 return 239 end 240 241-- block until focus is ack:ed 242 if not wnd.states.focused then 243 return 244 end 245 246 if wnd.block_release == button and not active then 247 wnd.block_release = nil 248 return 249 end 250 251 target_input(wnd.vid, { 252 kind = "digital", 253 mouse = true, 254 devid = 0, 255 subid = button, 256 active = active, 257 }) 258end 259 260local function wnd_mouse_drop(wnd) 261 wnd:drag_resize() 262end 263 264local function wnd_mouse_motion(wnd, vid, x, y, rx, ry) 265 local tx = x - wnd.x 266 local ty = y - wnd.y 267 268 target_input(wnd.vid, { 269 kind = "analog", mouse = true, 270 devid = 0, subid = 0, 271 samples = {tx, rx} 272 }) 273 274 target_input(wnd.vid, { 275 kind = "analog", mouse = true, 276 devid = 0, subid = 1, 277 samples = {ty, ry} 278 }) 279end 280 281local function wnd_mouse_drag(wnd, vid, dx, dy) 282-- also need to cover 'cursor-tagging' hint here for drag and drop 283 284 if wnd.states.moving then 285 local x, y = wnd.wm.move(wnd, wnd.x + dx, wnd.y + dy) 286 wnd.x = x 287 wnd.y = y 288 289-- for x11 we also need to message the new position 290 if wnd.send_position then 291 local msg = string.format("kind=move:x=%d:y=%d", wnd.x, wnd.y); 292 wnd.wm.log("wl_x11", msg) 293 target_input(vid, msg) 294 end 295 296 move_image(wnd.vid, wnd.x, wnd.y) 297 return 298 299--w when this is set, the resizing[] mask is also set 300 elseif wnd.states.resizing then 301 wnd:drag_resize(dx, dy) 302 303-- need to clamp to something 304 if wnd.in_resize[1] < 32 then 305 wnd.in_resize[1] = 32 306 end 307 308 if wnd.in_resize[2] < 32 then 309 wnd.in_resize[2] = 32 310 end 311 312 target_displayhint(wnd.vid, wnd.in_resize[1], wnd.in_resize[2]) 313 314-- re-emit as motion 315 else 316 local mx, my = mouse_xy() 317 wnd_mouse_motion(wnd, vid, mx, my) 318 end 319end 320 321local function wnd_hint_state(wnd) 322 local mask = 0 323 if not wnd.states.focused then 324 mask = bit.bor(mask, TD_HINT_UNFOCUSED) 325 end 326 327 if not wnd.states.visible then 328 mask = bit.bor(mask, TD_HINT_INVISIBLE) 329 end 330 331 if wnd.states.maximized then 332 mask = bit.bor(mask, TD_HINT_MAXIMIZED) 333 end 334 335 if wnd.states.fullscreen then 336 mask = bit.bor(mask, TD_HINT_FULLSCREEN) 337 end 338 339 return mask 340end 341 342local function wnd_unfocus(wnd) 343 wnd.wm.log(wnd.name, wnd.wm.fmt("focus=off:idstr=%s", wnd.idstr and wnd.idstr or wnd.name)) 344 wnd.states.focused = false 345 target_displayhint(wnd.vid, 0, 0, wnd_hint_state(wnd)) 346 wnd.wm.custom_cursor = false 347 mouse_switch_cursor("default") 348end 349 350local function wnd_focus(wnd) 351 wnd.wm.log(wnd.name, wnd.wm.fmt("focus=on:idstr=%s", wnd.idstr and wnd.idstr or wnd.name)) 352 wnd.states.focused = true 353 target_displayhint(wnd.vid, 0, 0, wnd_hint_state(wnd)) 354 wnd.wm.custom_cursor = wnd 355 table.remove_match(wnd.wm.window_stack, wnd) 356 table.insert(wnd.wm.window_stack, wnd) 357 wnd.wm:restack() 358end 359 360local function wnd_destroy(wnd) 361 wnd.wm.log(wnd.name, "destroy") 362 mouse_droplistener(wnd) 363 wnd.wm.windows[wnd.cookie] = nil 364 table.remove_match(wnd.wm.window_stack, wnd) 365 wnd.wm:restack() 366 367 if wnd.wm.custom_cursor == wnd then 368 mouse_switch_cursor("default") 369 end 370 if wnd.wm.cfg.destroy then 371 wnd.wm.cfg.destroy(wnd) 372 end 373 if valid_vid(wnd.vid) then 374 wnd.wm.known_surfaces[wnd.vid] = nil 375 delete_image(wnd.vid) 376 end 377end 378 379local function wnd_fullscreen(wnd) 380 if wnd.states.fullscreen then 381 return 382 end 383 384 wnd.states.fullscreen = {wnd.w, wnd.h, wnd.x, wnd.y} 385 wnd.defer_move = {0, 0} 386 target_displayhint(wnd.vid, 387 wnd.wm.disptbl.width, wnd.wm.disptbl.height, wnd_hint_state(wnd)) 388end 389 390local function wnd_maximize(wnd) 391 if wnd.states.maximized then 392 return 393 end 394 395-- drop fullscreen if we have it, but block hint 396 if wnd.states.fullscreen then 397 wnd:revert({no_hint = true}) 398 end 399 400 wnd.states.maximized = {wnd.w, wnd.h, wnd.x, wnd.y} 401 wnd.defer_move = {0, 0} 402 403 target_displayhint(wnd.vid, 404 wnd.wm.disptbl.width, wnd.wm.disptbl.height, wnd_hint_state(wnd)) 405end 406 407local function wnd_revert(wnd, opts) 408 local tbl 409 410-- drop fullscreen to maximized or 'normal' (the attributes stack) 411 if wnd.states.fullscreen then 412 tbl = wnd.states.fullscreen 413 wnd.states.fullscreen = false 414 415 elseif wnd.states.maximized then 416 tbl = wnd.states.maximized 417 wnd.states.maximized = false 418 else 419 return 420 end 421 422-- the better way of doing this is probably to enable detailed frame 423-- reporting for the surface, and have a state hook after this request 424-- an edge case is where the maximized dimensions correspond to the 425-- unmaximized ones which may be possible if there are no server-side 426-- decorations. 427 if not opts or not opts.no_hint then 428 target_displayhint(wnd.vid, tbl[1], tbl[2], wnd_hint_state(wnd)) 429 wnd.defer_move = {tbl[3], tbl[4]} 430 end 431 432 wnd_hint_state(wnd) 433end 434 435local function tl_wnd_resized(wnd, source, status) 436 if not wnd.states.mapped then 437 wnd.states.mapped = true 438 show_image(wnd.vid) 439 wnd.wm.mapped(wnd) 440 end 441 442-- special handling for drag-resize where we request a move 443 local rzmask = wnd.states.resizing 444 if rzmask then 445 local dw = (wnd.w - status.width) 446 local dh = (wnd.h - status.height) 447 local dx = dw * rzmask[3] 448 local dy = dh * rzmask[4] 449 450-- the move handler should account for padding 451 local x, y = wnd.wm.move(wnd, wnd.x + dx, wnd.y + dy) 452 wnd.x = x 453 wnd.y = y 454 move_image(wnd.vid, wnd.x, wnd.y) 455 wnd.defer_move = nil 456 end 457 458-- special case for state changes (maximized / fullscreen) 459 wnd.w = status.width 460 wnd.h = status.height 461 resize_image(wnd.vid, status.width, status.height) 462 463 if wnd.use_decor then 464 wnd.wm.decorate(wnd, wnd.vid, wnd.w, wnd.h) 465 end 466 467 if wnd.defer_move then 468 local x, y = wnd.wm.move(wnd, wnd.defer_move[1], wnd.defer_move[2]) 469 move_image(wnd.vid, x, y) 470 wnd.x = x 471 wnd.y = y 472 wnd.defer_move = nil 473 end 474end 475 476local function self_own(self, vid) 477-- if we are matching or a grab exists and we hold the grab or there is some proxy 478 return self.vid == vid or (self.mouse_proxy and vid == self.mouse_proxy) 479end 480 481local function x11_wnd_realize(wnd, popup, grab) 482 if wnd.realized then 483 return 484 end 485 486 if not wnd.states.mapped or not wnd.states.typed then 487 hide_image(wnd.vid) 488 return 489 end 490 491 show_image(wnd.vid) 492 target_displayhint(wnd.vid, wnd.w, wnd.h) 493 mouse_addlistener(wnd, {"motion", "drag", "drop", "button", "over", "out"}) 494 table.insert(wnd.wm.window_stack, 1, wnd) 495 496 wnd.wm.state_change(wnd, "realized", popup, grab) 497 wnd.realized = true 498end 499 500local function x11_wnd_type(wnd) 501 local popup_type = 502 wnd.states.typed == "menu" 503 or wnd.states.typed == "popup" 504 or wnd.states.typed == "tooltip" 505 or wnd.states.typed == "dropdown" 506 507-- icccm says about stacking order: 508-- wm_type_desktop < state_below < (no type) < dock | state_above < fullscreen 509-- other options, dnd, splash, utility (persistent_for) 510 511 if popup_type then 512 wnd.use_decor = false 513 wnd.wm.decorate(wnd) 514 image_inherit_order(wnd.vid, false) 515 order_image(wnd.vid, 65531) 516 else 517-- decorate should come from toplevel 518 image_inherit_order(wnd.vid, true) 519 wnd.use_decor = true 520 order_image(wnd.vid, 1) 521 end 522 523 wnd:realize() 524end 525 526local function x11_nudge(wnd, dx, dy) 527 local x, y = wnd.wm.move(wnd, wnd.x + dx, wnd.y + dy, dx, dy) 528 move_image(wnd.vid, x, y) 529 wnd.x = x 530 wnd.y = y 531 local msg = string.format("kind=move:x=%d:y=%d", x, y); 532 533 wnd.wm.log("wl_x11", msg) 534 target_input(wnd.vid, msg) 535end 536 537local function wnd_nudge(wnd, dx, dy) 538 local x, y = wnd.wm.move(wnd, wnd.x + dx, wnd.y + dy, dx, dy) 539 move_image(wnd.vid, x, y) 540 wnd.x = x 541 wnd.y = y 542 wnd.wm.log("wnd", wnd.wm.fmt("source=%d:x=%d:y=%d", wnd.vid, x, y)) 543end 544 545local function wnd_drag_rz(wnd, dx, dy, mx, my) 546 if not dx then 547 wnd.states.resizing = false 548 wnd.in_resize = nil 549 return 550 end 551 552 if not wnd.in_resize then 553 wnd.in_resize = {wnd.w, wnd.h} 554 if (mx and my) then 555 wnd.states.resizing = {1, 1, mx, my} 556 end 557 end 558-- apply direction mask, clamp against lower / upper constraints 559 local tw = wnd.in_resize[1] + (dx * wnd.states.resizing[1]) 560 local th = wnd.in_resize[2] + (dy * wnd.states.resizing[2]) 561 562 if tw < wnd.min_w then 563 tw = wnd.min_w 564 end 565 566 if th < wnd.min_h then 567 th = wnd.min_h 568 end 569 570 if wnd.max_w > 0 and tw > wnd.max_w then 571 tw = wnd.max_w 572 end 573 574 if wnd.max_h > 0 and tw > wnd.max_h then 575 tw = wnd.max_h 576 end 577 578 wnd.in_resize = {tw, th} 579 target_displayhint(wnd.vid, wnd.in_resize[1], wnd.in_resize[2]) 580 wnd.wm.log("wnd", wnd.wm.fmt("source=%d:drag_rz=%d:%d", 581 wnd.vid, wnd.in_resize[1], wnd.in_resize[2])) 582end 583 584-- several special considerations with x11, particularly that some 585-- things are positioned based on a global 'root' anchor, a rather 586-- involved type model and a number of wm messages that we need to 587-- respond to. 588-- 589-- another is that we need to decorate window contents ourselves, 590-- with all the jazz that entails. 591local function x11_vtable() 592 return { 593 name = "x11_bridge", 594 own = self_own, 595 x = 0, 596 y = 0, 597 w = 32, 598 h = 32, 599 pad_x = 0, 600 pad_y = 0, 601 min_w = 32, 602 min_h = 32, 603 max_w = 0, 604 max_h = 0, 605 send_position = true, 606 use_decor = true, 607 608 states = { 609 mapped = false, 610 typed = false, 611 fullscreen = false, 612 maximized = false, 613 visible = false, 614 moving = false, 615 resizing = false 616 }, 617 618-- assumes a basic 'window' then we patch things around when we have 619-- been assigned a type / mapped - default is similar to wayland toplevel 620-- with added messaging about window coordinates within the space 621 destroy = wnd_destroy, 622 input_table = wnd_input_table, 623 624 over = wnd_mouse_over, 625 out = wnd_mouse_out, 626 button = wnd_mouse_btn, 627 drag = x11_mouse_drag, 628 motion = wnd_mouse_motion, 629 drop = wnd_mouse_drop, 630 631 focus = wnd_focus, 632 unfocus = wnd_unfocus, 633 revert = wnd_revert, 634 fullscreen = wnd_fullscreen, 635 maximize = wnd_maximize, 636 drag_resize = wnd_drag_rz, 637 nudge = x11_nudge, 638 apply_type = x11_wnd_type, 639 realize = x11_wnd_realize 640 } 641end 642 643local function wnd_dnd_source(wnd, x, y, types) 644-- this needs the 'cursor-tag' attribute when we enter, then some 645-- marking function to say if it is desired or not .. 646end 647 648local function wnd_copy_paste(wnd, src, dst) 649 local nc = #src.states.copy_set 650 if not dst.wm or not valid_vid(dst.wm.control) or nc == 0 then 651 return 652 end 653 654 local control = dst.wm.control 655 656-- send src to copy client, mark as 'latest' src, clamp number of offers 657 target_input(control, "offer") 658 local lim = nc < 32 and nc or 32 659 for i=1,lim do 660 target_input(control, v) 661 end 662 target_input(control, "/offer") 663 664-- when the client has picked an offer, that will come back in the wm 665-- handler on dst and initiate the paste operation then 666 dst.wm.offer_src = src 667end 668 669local function tl_vtable(wm) 670 return { 671 name = "wl_toplevel", 672 wm = wm, 673 674-- states that need to be configurable from the WM and forwarded to the 675-- client so that it can update decorations or modify its input response 676 states = { 677 mapped = false, 678 focused = false, 679 fullscreen = false, 680 maximized = false, 681 visible = false, 682 moving = false, 683 resizing = false, 684 copy_set = {} 685 }, 686 687-- wm side calls these to acknowledge or initiat estate changes 688 focus = wnd_focus, 689 unfocus = wnd_unfocus, 690 maximize = wnd_maximize, 691 fullscreen = wnd_fullscreen, 692 revert = wnd_revert, 693 nudge = wnd_nudge, 694 drag_resize = wnd_drag_rz, 695 dnd_source = wnd_dnd_source, 696 copy_paste = wnd_paste_opts, 697 698-- properties that needs to be tracked for drag-resize/drag-move 699 x = 0, 700 y = 0, 701 w = 0, 702 h = 0, 703 min_w = 32, 704 min_h = 32, 705 max_w = 0, 706 max_h = 0, 707 708-- keyboard input 709 input_table = wnd_input_table, 710 711-- touch-mouse input goes here 712 over = wnd_mouse_over, 713 out = wnd_mouse_out, 714 drag = wnd_mouse_drag, 715 drop = wnd_mouse_drop, 716 button = wnd_mouse_btn, 717 motion = wnd_mouse_motion, 718 destroy = wnd_destroy, 719 own = self_own 720 } 721end 722 723local function popup_click(popup, vid, x, y) 724 local tbl = 725 target_input(vid, { 726 kind = "digital", 727 mouse = true, 728 devid = 0, 729 subid = 1, 730 active = true, 731 }) 732 target_input(vid, { 733 kind = "digital", 734 mouse = true, 735 devid = 0, 736 subid = 1, 737 active = false, 738 }) 739end 740 741-- put an invisible surface at the overlay level and add a mouse-handler that 742-- calls a destroy function if clicked. 743local function setup_grab_surface(popup) 744 local vid = null_surface(popup.wm.disptbl.width, popup.wm.disptbl.height) 745 rendertarget_attach(popup.wm.disptbl.rt, vid, RENDERTARGET_DETACH) 746 747 show_image(vid) 748 order_image(vid, 65530) 749 image_tracetag(vid, "popup_grab") 750 751 local done = false 752 local tbl = { 753 name = "popup_grab_mh", 754 own = function(ctx, tgt) 755 return vid == tgt 756 end, 757 button = function() 758 if not done then 759 done = true 760 popup:destroy() 761 end 762 end 763 } 764 mouse_addlistener(tbl, {"button"}) 765 popup.wm.log("popup", popup.wm.fmt("grab_on")) 766 767 return function() 768 popup.wm.log("popup", popup.wm.fmt("grab_free")) 769 mouse_droplistener(tbl) 770 delete_image(vid) 771 done = true 772 end 773end 774 775local function popup_destroy(popup) 776 if popup.grab then 777 popup.grab = popup.grab() 778 end 779 780 mouse_switch_cursor("default") 781 782-- might have chain-destroyed through the parent vid or terminated on its own 783 if valid_vid(popup.vid) then 784 delete_image(popup.vid) 785 popup.wm.known_surfaces[popup.vid] = nil 786 end 787 788 mouse_droplistener(popup) 789 popup.wm.windows[popup.cookie] = nil 790end 791 792local function popup_over(popup) 793-- this might have changed with mouse_out 794 if popup.wm.cursor then 795 mouse_custom_cursor(popup.wm.cursor) 796 else 797 mouse_switch_cursor("default") 798 end 799end 800 801local function popup_vtable() 802 return { 803 name = "popup_mh", 804 own = self_own, 805 motion = wnd_mouse_motion, 806 click = popup_click, 807 destroy = popup_destroy, 808 over = popup_over, 809 out = popup_out, 810 states = { 811 }, 812 x = 0, 813 y = 0 814 } 815end 816 817local function on_popup(popup, source, status) 818 if status.kind == "create" then 819 local wnd = popup 820 821-- if we have a popup that has not been assigned to anything when we get 822-- the next one already, not entirely 100% whether that is permitted, and 823-- more importantly, actually used/sanctioned behaviour 824 if wnd.pending_popup and valid_vid(wnd.pending_popup.vid) then 825 wnd.pending_popup:destroy() 826 end 827 828 local popup = popup_vtable() 829 local vid, aid, cookie = 830 accept_target( 831 function(...) 832 return on_popup(popup, ...) 833 end 834 ) 835 image_tracetag(vid, "wl_popup") 836 rendertarget_attach(wnd.disptbl.rt, vid, RENDERTARGET_DETACH) 837 838 wnd.known_surfaces[vid] = true 839 wnd.pending_popup = popup 840 link_image(vid, wnd.anchor) 841 popup.wm = wnd 842 popup.cookie = cookie 843 popup.vid = vid 844 845-- also not entirely sure if popup-drag-n-drop behavior is a thing, so just 846-- map clicks and motion for the time being 847 mouse_addlistener(popup, {"motion", "click", "over", "out"}) 848 849 elseif status.kind == "terminated" then 850 popup:destroy() 851 852 elseif status.kind == "resized" then 853 854-- wait with showing the popup until it is both viewported and mapped 855 if not popup.states.mapped then 856 popup.states.mapped = true 857 if popup.got_parent then 858 show_image(popup.vid) 859 end 860 end 861 resize_image(popup.vid, status.width, status.height) 862 863 elseif status.kind == "viewport" then 864 local pwnd = popup.wm.windows[status.parent] 865 if not pwnd then 866 popup.wm.log("popup", popup.wm.fmt("bad_parent=%d", status.parent)) 867 popup.got_parent = false 868 hide_image(popup.vid) 869 return 870 end 871 872 pwnd.popup = popup 873 popup.parent = pwnd 874 popup.got_parent = true 875 876-- more anchoring and positioning considerations here 877 link_image(popup.vid, pwnd.vid) 878 move_image(popup.vid, status.rel_x, status.rel_y) 879 880-- 'popups' can be used for tooltips and so on as well, take that into account 881-- as well as enable a 'grab' layer that lives with the focused popup 882 if status.focus then 883 order_image(popup.vid, 65531) 884 if not popup.grab then 885 popup.grab = setup_grab_surface(popup) 886 end 887 image_mask_clear(popup.vid, MASK_UNPICKABLE) 888 else 889-- release any existing grab 890 if popup.grab then 891 popup.grab = popup.grab() 892 end 893 order_image(popup.vid, 1) 894 image_mask_set(popup.vid, MASK_UNPICKABLE) 895 end 896 897-- this needs to be synched if the window is moved through code/wm 898 local props = image_surface_resolve(popup.vid) 899 popup.x = props.x 900 popup.y = props.y 901 902-- possible animation hook 903 if popup.states.mapped then 904 show_image(popup.vid) 905 end 906 907 if popup.wm.pending_popup == popup then 908 popup.wm.pending_popup = nil 909 end 910 end 911end 912 913local function on_toplevel(wnd, source, status) 914 if status.kind == "create" then 915 local new = tl_vtable() 916 new.wm = wnd 917 918-- request dimensions from wm 919 local w, h, x, y = wnd.configure(new, "toplevel") 920 local vid, aid, cookie = 921 accept_target(w, h, 922 function(...) 923 return on_toplevel(new, ...) 924 end 925 ) 926 rendertarget_attach(wnd.disptbl.rt, vid, RENDERTARGET_DETACH) 927 new.vid = vid 928 new.cookie = cookie 929 wnd.known_surfaces[vid] = true 930 new.x = x 931 new.y = y 932 table.insert(wnd.window_stack, new) 933 934-- tie to bridge as visibility / clipping anchor 935 image_tracetag(vid, "wl_toplevel") 936 new.vid = vid 937 image_inherit_order(vid, true) 938 link_image(vid, wnd.anchor) 939 940-- register mouse handler 941 mouse_addlistener(new, {"over", "out", "drag", "button", "motion", "drop"}) 942 943 return new, cookie 944 945 elseif status.kind == "terminated" then 946 wnd:destroy() 947 948-- might need to add frame delivery notification so that we can track/clear 949-- parts that have 'double buffered' state 950 951 elseif status.kind == "resized" then 952-- first time showing 953 tl_wnd_resized(wnd, source, status) 954 955-- viewport is used here to define another window as the current toplevel, 956-- i.e. that should cause this window to hide or otherwise be masked, and 957-- the parent set to order above it (until unset) 958 elseif status.kind == "viewport" then 959 local parent = wnd.wm.windows[status.parent] 960 if parent then 961 wnd.wm.log("wl_toplevel", wnd.wm.fmt("reparent=%d",status.parent)) 962 wnd.wm.state_change(wnd, "toplevel", parent) 963 else 964 wnd.wm.log("wl_toplevel", wnd.wm.fmt("viewport:unknown_parent:%d", status.parent)) 965 end 966 967-- wl specific wm hacks 968 elseif status.kind == "message" then 969 wnd.wm.log("wl_toplevel", wnd.wm.fmt("message=%s", status.message)) 970 local opts = string.split(status.message, ':') 971 if not opts or not opts[1] then 972 return 973 end 974 975 if 976 opts[1] == "shell" and 977 opts[2] == "xdg_top" and 978 opts[3] and wl_top_lut[opts[3]] then 979 wl_top_lut[opts[3]](wnd, source, unpack(opts, 4)) 980 end 981 end 982end 983 984local function on_cursor(ctx, source, status) 985 if status.kind == "create" then 986 local cursor = accept_target( 987 function(...) 988 return on_cursor(ctx, ...) 989 end) 990 991 ctx.cursor.vid = cursor 992 link_image(ctx.bridge, cursor) 993 ctx.known_surfaces[cursor] = true 994 image_tracetag(cursor, "wl_cursor") 995 rendertarget_attach(ctx.disptbl.rt, cursor, RENDERTARGET_DETACH) 996 997 elseif status.kind == "resized" then 998 ctx.cursor.width = status.width 999 ctx.cursor.height = status.height 1000 resize_image(ctx.cursor.vid, status.width, status.height) 1001 1002 if ctx.custom_cursor then 1003 mouse_custom_cursor(ctx.cursor) 1004 end 1005 1006 elseif status.kind == "message" then 1007-- hot-spot modification? 1008 if ctx.custom_cursor then 1009 mouse_custom_cursor(ctx.cursor) 1010 end 1011 1012 elseif status.kind == "terminated" then 1013 delete_image(source) 1014 ctx.known_surfaces[source] = nil 1015 end 1016end 1017 1018-- fixme: incomplete, input routing, attachments etc. needed 1019local function on_subsurface(ctx, source, status) 1020 if status.kind == "create" then 1021 local subwnd = { 1022 name = "tl_subsurface" 1023 } 1024 local vid, aid, cookie = 1025 accept_target( 1026 function(...) 1027 return on_subsurface(subwnd, ...) 1028 end 1029 ) 1030 subwnd.vid = vid 1031 subwnd.wm = ctx 1032 subwnd.cookie = cookie 1033 ctx.wm.known_surfaces[vid] = true 1034 rendertarget_attach(ctx.wm.disptbl.rt, vid, RENDERTARGET_DETACH) 1035 image_tracetag(vid, "wl_subsurface") 1036 1037-- subsurfaces need a parent to attach to and 'extend', 1038-- input should be translated into its coordinate space as well 1039 return subwnd, cookie 1040 elseif status.kind == "resized" then 1041 1042 elseif status.kind == "viewport" then 1043 link_image(source, parent.vid) 1044 1045 elseif status.kind == "terminated" then 1046 delete_image(source) 1047 ctx.wm.windows[ctx.cookie] = nil 1048 ctx.wm.known_surfaces[source] = nil 1049 end 1050end 1051 1052local function x11_viewport(wnd, source, status) 1053 local anchor = wnd.wm.anchor 1054 if status.parent ~= 0 then 1055 local pwnd = wnd.wm.windows[status.parent] 1056 if pwnd then 1057 anchor = pwnd.vid 1058 end 1059 end 1060 1061-- ignore repositioning hints while we are dragging 1062 if wnd.in_resize then 1063 return 1064 end 1065 1066-- depending on type, we need to order around as well 1067 link_image(wnd.vid, anchor) 1068 move_image(wnd.vid, status.rel_x, status.rel_y) 1069 1070 local props = image_surface_resolve(wnd.vid) 1071 local x, y = wnd.wm.move(wnd, props.x, props.y) 1072 wnd.x = x 1073 wnd.y = y 1074 1075 wnd.wm.log("wl_x11", wnd.wm.fmt( 1076 "viewport:parent=%d:hx=%d:hy=%d:x=%d:y=%d", 1077 status.parent, status.rel_x, status.rel_y, x, y) 1078 ) 1079 1080-- we need something more here to protect against an infinite move-loop 1081-- target_input(wnd.vid, string.format("kind=move:x=%d:y=%d", x, y)) 1082end 1083 1084local function on_x11(wnd, source, status) 1085-- most involved here as the meta-WM forwards a lot of information 1086 if status.kind == "create" then 1087 local x11 = x11_vtable() 1088 x11.wm = wnd 1089 1090 local w, h, x, y = wnd.configure(x11, "x11") 1091 1092 local vid, aid, cookie = 1093 accept_target(w, h, 1094 function(...) 1095 return on_x11(x11, ...) 1096 end) 1097 rendertarget_attach(wnd.disptbl.rt, vid, RENDERTARGET_DETACH) 1098 1099 x11.x = x 1100 x11.y = y 1101 move_image(vid, x, y) 1102 1103 wnd.known_surfaces[vid] = true 1104 1105-- send our preset position, might not matter if it is override-redirect 1106-- this was removed as it caused a race condition, 'create' doesn't mean that 1107-- the window is realized yet, so defer that until it happens 1108-- 1109-- local msg = string.format("kind=move:x=%d:y=%d", x, y) 1110-- wnd.log("wl_x11", msg) 1111-- target_input(vid, msg) 1112-- show_image(vid) 1113 1114 x11.vid = vid 1115 x11.cookie = cookie 1116 image_tracetag(vid, "x11_unknown_type") 1117 image_inherit_order(vid, true) 1118 link_image(vid, wnd.anchor) 1119 1120 return x11, cookie 1121 1122 elseif status.kind == "resized" then 1123 tl_wnd_resized(wnd, source, status) 1124 wnd:realize() 1125 1126-- let the caller decide how we deal with decoration 1127 if wnd.realized and wnd.use_decor then 1128 local t, l, d, r = wnd.wm.decorate(wnd, wnd.vid, wnd.w, wnd.h) 1129 wnd.pad_x = t 1130 wnd.pad_y = l 1131 end 1132 1133 elseif status.kind == "message" then 1134 local opts = string.split(status.message, ':') 1135 if not opts or not opts[1] or not x11_lut[opts[1]] then 1136 wnd.wm.log("wl_x11", wnd.wm.fmt("unhandled_message=%s", status.message)) 1137 return 1138 end 1139 wnd.wm.log("wl_x11", wnd.wm.fmt("message=%s", status.message)) 1140 return x11_lut[opts[1]](wnd, source, unpack(opts, 2)) 1141 1142 elseif status.kind == "registered" then 1143 wnd.guid = status.guid 1144 1145 elseif status.kind == "viewport" then 1146 x11_viewport(wnd, source, status) 1147 1148 elseif status.kind == "terminated" then 1149 wnd:destroy() 1150 end 1151end 1152 1153local function bridge_handler(ctx, source, status) 1154 if status.kind == "terminated" then 1155 ctx:destroy() 1156 return 1157 1158-- message on the bridge is also used to pass metadata about the 'data-device' 1159-- properties like selection changes and type 1160 elseif status.kind == "message" then 1161 local cmd, data = string.split_first(status.message, ":") 1162 ctx.log("bridge", ctx.fmt("message:kind=%s", cmd)) 1163 if cmd == "offer" then 1164 if table.find_i(ctx.offer, data) then 1165 return 1166 end 1167 1168-- normal 'behavior' here is that the data source provides a stream of types that 1169-- supposedly is mime, in reality is a mix of whatever, on 'paste' or drag-enter 1170-- you forward these to the destination, it says which one it is, then send 1171-- descriptors in both directions. Since the WM might want to do things with the 1172-- clipboard contents - notify here and provide the methods to forward / trigger 1173-- an offer. 1174 table.insert(ctx.offer, data) 1175 1176 elseif cmd == "offer-reset" then 1177 ctx.offer = {} 1178 end 1179 1180 return 1181 elseif status.kind ~= "segment_request" then 1182 return 1183 end 1184 1185 local permitted = { 1186 cursor = on_cursor, 1187 application = on_toplevel, 1188 popup = on_popup, 1189 multimedia = on_subsurface, -- fixme: still missing 1190 ["bridge-x11"] = on_x11 1191 } 1192 1193 local handler = permitted[status.segkind] 1194 if not handler then 1195 warning("unhandled segment type: " .. status.segkind) 1196 return 1197 end 1198 1199-- actual allocation is deferred to the specific handler, some might need to 1200-- call back into the outer WM to get suggested default size/position - x11 1201-- clients need world-space coordinates and so on 1202 local wnd, cookie = handler(ctx, source, {kind = "create"}) 1203 if wnd then 1204 ctx.windows[cookie] = wnd 1205 end 1206end 1207 1208local function set_rate(ctx, period, delay) 1209 message_target(ctx.bridge, 1210 string.format("seat:rate:%d,%d", period, delay)) 1211end 1212 1213-- first wayland node, limited handler that can only absorb meta info, 1214-- act as clipboard and allocation proxy 1215local function set_bridge(ctx, source) 1216 local w = ctx.disptbl.width 1217 local h = ctx.disptbl.height 1218 1219 target_displayhint(source, w, h, 0, ctx.disptbl) 1220 1221-- wl_drm need to be able to authenticate against the GPU, which may 1222-- have security implications for some people - low risk enough for 1223-- opt-out rather than in 1224 if not ctx.cfg.block_gpu then 1225 target_flags(source, TARGET_ALLOWGPU) 1226 end 1227 1228 target_updatehandler(source, 1229 function(...) 1230 return bridge_handler(ctx, ...) 1231 end 1232 ) 1233 1234 ctx.bridge = source 1235 ctx.anchor = null_surface(w, h) 1236 image_tracetag(ctx.anchor, "wl_bridge_anchor") 1237 image_mask_set(ctx.anchor, MASK_UNPICKABLE) 1238 1239 show_image(ctx.anchor) 1240 ctx.mh = { 1241 name = "wl_bg", 1242 own = self_own, 1243 vid = ctx.anchor, 1244 click = function() 1245 ctx.focus() 1246 end, 1247 } 1248 mouse_addlistener(ctx.mh, {"click"}) 1249 1250-- ctx:repeat_rate(ctx.cfg.repeat, ctx.cfg.delay) 1251end 1252 1253local function resize_output(ctx, neww, newh, density, refresh) 1254 if density then 1255 ctx.disptbl.vppcm = density 1256 ctx.disptbl.hppcm = density 1257 end 1258 1259 if neww then 1260 ctx.disptbl.width = neww 1261 else 1262 neww = ctx.disptbl.width 1263 end 1264 1265 if newh then 1266 ctx.disptbl.height = newh 1267 else 1268 newh = ctx.disptbl.height 1269 end 1270 1271 if refresh then 1272 ctx.disptbl.refresh = refresh 1273 end 1274 1275 if not valid_vid(ctx.bridge) then 1276 return 1277 end 1278 1279 ctx.log("bridge", ctx.fmt("output_resize=%d:%d", ctx.disptbl.width, ctx.disptbl.height)) 1280 target_displayhint(ctx.bridge, neww, newh, 0, ctx.disptbl) 1281 1282-- tell all windows that some of their display parameters have changed, 1283-- if the window is in fullscreen/maximized state - the surface should 1284-- be resized as well 1285 for _, v in pairs(ctx.windows) do 1286-- this will fetch the refreshed display table 1287 if v.reconfigure then 1288 if v.states.fullscreen or v.states.maximized then 1289 v:reconfigure(neww, newh) 1290 else 1291 v:reconfigure(v.w, v.h) 1292 end 1293 end 1294 end 1295end 1296 1297local function reparent_rt(ctx, rt) 1298 ctx.disptbl.rt = rt 1299 for k,v in pairs(ctx.known_surfaces) do 1300 rendertarget_attach(ctx.disptbl.rt, k, RENDERTARGET_DETACH) 1301 end 1302 if valid_vid(ctx.anchor) then 1303 rendertarget_attach(ctx.disptbl.rt, ctx.anchor, RENDERTARGET_DETACH) 1304 end 1305end 1306 1307local window_stack = {} 1308local function restack(ctx) 1309 local cnt = 0 1310 for _, v in pairs(ctx.windows) do 1311 cnt = cnt + 1 1312 end 1313-- re-order 1314 for i,v in ipairs(ctx.window_stack) do 1315 order_image(v.vid, i * 10); 1316 end 1317end 1318 1319local function bridge_table(cfg) 1320 local res = { 1321-- vid to the client bridge 1322 control = BADID, 1323 1324-- The window stack is (default) global for all bridges for ordering to work, 1325-- each window is reserved 10 order slots. 1326 window_stack = window_stack, 1327 1328-- key indexed on window identifier cookie 1329 windows = {}, 1330 1331-- tracks all externally allocated VIDs 1332 known_surfaces = {}, 1333 1334-- currently active cursor on seat 1335 cursor = { 1336 vid = BADID, 1337 hotspot_x = 0, 1338 hotspot_y = 0, 1339 width = 1, 1340 height = 1 1341 }, 1342 1343 offer = { 1344 }, 1345 1346-- user table of settings 1347 cfg = cfg, 1348 1349-- last known 'output' properties (vppcm, refresh also possible) 1350 disptbl = { 1351 rt = WORLDID, 1352 width = VRESW, 1353 height = VRESH, 1354 ppcm = VPPCM 1355 }, 1356 1357-- call when output properties have changed 1358 resize = resize_output, 1359 1360-- call to update keyboard state knowledge 1361 repeat_rate = set_rate, 1362 1363-- called internally whenever the window stack has changed 1364 restack = restack, 1365 1366-- call to switch attachment to a specific rendertarget 1367 set_rt = reparent_rt, 1368 1369-- swap out for logging / tracing function 1370 log = print, 1371 fmt = string.format 1372 } 1373 1374-- let client config override some defaults 1375 if cfg.width then 1376 res.disptbl.width = cfg.width 1377 end 1378 1379 if cfg.height then 1380 res.disptbl.height = cfg.height 1381 end 1382 1383 if cfg.fmt then 1384 res.fmt = cfg.fmt 1385 end 1386 1387 if cfg.log then 1388 res.log = cfg.log 1389 end 1390 1391 if type(cfg.window_stack) == "table" then 1392 res.window_stack = cfg.window_stack 1393 end 1394 1395-- add client defined event handlers, provide default inplementations if missing 1396 if type(cfg.move) == "function" then 1397 res.log("wlwm", "override_handler=move") 1398 res.move = cfg.move 1399 else 1400 res.log("wlwm", "default_handler=move") 1401 res.move = 1402 function(wnd, x, y, dx, dy) 1403 return x, y 1404 end 1405 end 1406 1407 if type(cfg.context_menu) == "function" then 1408 res.log("wlwm", "override_handler=context_menu") 1409 res.context_menu = cfg.context_menu 1410 else 1411 res.context_menu = function() 1412 end 1413 end 1414 1415 res.configure = 1416 function(...) 1417 local w, h, x, y 1418 if cfg.configure then 1419 w, h, x, y = cfg.configure(...) 1420 end 1421 w = w and w or res.disptbl.width * 0.5 1422 h = h and h or res.disptbl.height * 0.3 1423 if not x or not y then 1424 x, y = mouse_xy() 1425 end 1426 return w, h, x, y 1427 end 1428 1429 if type(cfg.focus) == "function" then 1430 res.log("wlwm", "override_handler=focus") 1431 res.focus = cfg.focus 1432 else 1433 res.log("wlwm", "default_handler=focus") 1434 res.focus = 1435 function() 1436 return true 1437 end 1438 end 1439 1440 if type(cfg.decorate) == "function" then 1441 res.log("wlwm", "override_handler=decorate") 1442 res.decorate = cfg.decorate 1443 else 1444 res.log("wlwm", "default_handler=decorate") 1445 res.decorate = 1446 function() 1447 end 1448 end 1449 1450 if type(cfg.mapped) == "function" then 1451 res.log("wlwm", "override_handler=mapped") 1452 res.mapped = cfg.mapped 1453 else 1454 res.log("wlwm", "default_handler=mapped") 1455 res.mapped = 1456 function() 1457 end 1458 end 1459 1460 if type(cfg.state_change) == "function" then 1461 res.log("wlwm", "override_handler=state_change") 1462 res.state_change = cfg.state_change 1463 else 1464 res.state_change = 1465 function(wnd, state) 1466 if not state then 1467 wnd:revert() 1468 end 1469 1470 end 1471 end 1472 1473 if (cfg.resize_request) == "function" then 1474 res.log("wlwm", "override_handler=resize_request") 1475 res.resize_request = cfg.resize_request 1476 else 1477 res.log("wlwm", "default_handler=resize_request") 1478 res.resize_request = 1479 function(wnd, new_w, new_h) 1480 if new_w > ctx.disptbl.width then 1481 new_w = ctx.disptbl.width 1482 end 1483 1484 if new_h > ctx.disptbl.height then 1485 new_h = ctx.disptbl.height 1486 end 1487 1488 return new_w, new_h 1489 end 1490 end 1491 1492-- destroy always has a builtin handler and then cfg is optionally pulled in 1493 res.destroy = 1494 function() 1495 local rmlist = {} 1496 1497-- convert to in-order and destroy all windows first 1498 for k,v in pairs(res.windows) do 1499 table.insert(rmlist, v) 1500 end 1501 for i,v in ipairs(rmlist) do 1502 if v.destroy then 1503 v:destroy() 1504 if cfg.destroy then 1505 cfg.destroy(v) 1506 end 1507 end 1508 end 1509 1510 if cfg.destroy then 1511 cfg.destroy(res) 1512 end 1513 if valid_vid(res.bridge) then 1514 delete_image(res.bridge) 1515 end 1516 if valid_vid(res.anchor) then 1517 delete_image(res.anchor) 1518 end 1519 1520 mouse_droplistener(res) 1521 local keys = {} 1522 for k,v in pairs(res) do 1523 table.insert(keys, v) 1524 end 1525 for _,k in ipairs(keys) do 1526 res[k] = nil 1527 end 1528 end 1529 1530 return res 1531end 1532 1533local function client_handler(nested, trigger, source, status) 1534-- keep track of anyone that goes through here 1535 if not bridges[source] then 1536 bridges[source] = {} 1537 end 1538 1539 if status.kind == "registered" then 1540-- happens if we are forwarded from the wrong type (api error) 1541 if status.segkind ~= "bridge-wayland" then 1542 delete_image(source) 1543 return 1544 end 1545 1546-- we have a wayland bridge, and need to know if it is used to bootstrap other 1547-- clients or not - we see that if it requests a non-bridge-wayland type on its 1548-- next segment request 1549 elseif status.kind == "segment_request" then 1550 1551-- nested, only allow one 'level' 1552 if status.segkind == "bridge-wayland" then 1553 if nested then 1554 return false 1555 end 1556 1557-- next one will be the one to ask for 'real' windows 1558 local vid = 1559 accept_target(32, 32, 1560 function(...) 1561 return client_handler(true, trigger, ...) 1562 end) 1563 1564-- and those we just forward to the wayland factory 1565 else 1566 local bridge = trigger(source, status) 1567 if bridge then 1568 table.insert(bridges[source], bridge) 1569 rendertarget_attach(bridge.disptbl.rt, vid, RENDERTARGET_DETACH) 1570 end 1571 end 1572 1573-- died before getting anywhere meaningful - or is the bridge itself gone? 1574-- if so, kill off every client associated with it 1575 elseif status.kind == "terminated" then 1576 for k,v in ipairs(bridges[source]) do 1577 v:destroy() 1578 end 1579 delete_image(source) 1580 bridges[source] = nil 1581 end 1582end 1583 1584local function connection_mgmt(source, trigger) 1585 target_updatehandler(source, 1586 function(source, status) 1587 client_handler(false, trigger, source, status) 1588 end 1589 ) 1590end 1591 1592-- factory function is intended to be used when a bridge-wayland segment 1593-- requests something that is not a bridge-wayland, then the bridge (ref 1594-- by [vid]) will have its handler overridden and treated as a client. 1595return 1596function(vid, segreq, cfg) 1597 local ctx = bridge_table(cfg) 1598 set_bridge(ctx, vid) 1599 bridge_handler(ctx, vid, segreq) 1600 return ctx 1601end, connection_mgmt 1602