Class: Roxane::Window
- Inherits:
-
Object
- Object
- Roxane::Window
- Defined in:
- lib/roxane/window.rb
Overview
A native window hosting the system webview, with a Ruby<->JS operation bridge. The shell/transport only: it serves whatever frontend you give it and is agnostic to look-and-feel.
win = Roxane::Window.new(title: "Hello", size: [900, 600])
win.on("greet") { |name| "Hello, #{name}!" } # window.roxane.invoke("greet", "x")
win.emit("tick", { at: 1 }) # window.roxane.on("tick", cb)
win.serve(File.("ui")) # serve a built frontend dir
win.run
Constant Summary collapse
- DISPATCH =
the single C binding behind window.roxane.invoke
"__roxane_dispatch"
Instance Method Summary collapse
-
#bind(name, &block) ⇒ Object
Low-level escape hatch: expose window.
(...args) -> Promise. -
#close ⇒ Object
Stop the loop / close the window.
-
#emit(event, payload = nil) ⇒ Object
Push an event to the frontend (delivered to window.roxane.on(event, cb)).
-
#eval(js) ⇒ Object
Run arbitrary JS in the page.
- #html(markup) ⇒ Object
-
#initialize(title: "Roxane", size: [800, 600], min_size: nil, resizable: true, debug: false) ⇒ Window
constructor
A new instance of Window.
- #load(url) ⇒ Object
-
#on(op, &block) ⇒ Object
Handle window.roxane.invoke(op, payload).
- #on_close(&block) ⇒ Object
-
#run ⇒ Object
Run the native UI loop.
-
#serve(dir) ⇒ Object
Serve a built frontend directory over loopback http and load it.
-
#zoom ⇒ Object
Full-content zoom, like a browser's Ctrl+± (1.0 = natural size).
- #zoom=(factor) ⇒ Object
Constructor Details
#initialize(title: "Roxane", size: [800, 600], min_size: nil, resizable: true, debug: false) ⇒ Window
Returns a new instance of Window.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/roxane/window.rb', line 18 def initialize(title: "Roxane", size: [800, 600], min_size: nil, resizable: true, debug: false) unless FFI::AVAILABLE raise Error, "libwebview is not available (#{FFI::LOAD_ERROR}). " \ "Set ROXANE_LIBWEBVIEW or install the webview C library." end @handlers = {} # operation name (String) -> handler block @callbacks = [] # retained FFI::Function refs (must outlive the window) @on_close = nil @server = nil @ui_queue = Queue.new # procs to run on the UI thread @zoom = 1.0 @w = FFI.webview_create(debug ? 1 : 0, nil) raise Error, "could not create webview" if @w.null? FFI.webview_set_title(@w, title.to_s) apply_size(size, min_size, resizable) install_ui_pump install_bridge end |
Instance Method Details
#bind(name, &block) ⇒ Object
Low-level escape hatch: expose window.
62 63 64 65 66 67 68 69 |
# File 'lib/roxane/window.rb', line 62 def bind(name, &block) cb = ::FFI::Function.new(:void, [:string, :string, :pointer]) do |seq, req, _arg| run_handler(seq) { block.call(*JSON.parse(req)) } end @callbacks << cb FFI.webview_bind(@w, name.to_s, cb, nil) self end |
#close ⇒ Object
Stop the loop / close the window. Safe from any thread.
127 128 129 |
# File 'lib/roxane/window.rb', line 127 def close FFI.webview_terminate(@w) end |
#emit(event, payload = nil) ⇒ Object
Push an event to the frontend (delivered to window.roxane.on(event, cb)). Safe to call from any thread.
51 52 53 |
# File 'lib/roxane/window.rb', line 51 def emit(event, payload = nil) eval("window.roxane && window.roxane.__receive(#{JSON.generate(event.to_s)}, #{JSON.generate(payload)})") end |
#eval(js) ⇒ Object
Run arbitrary JS in the page. Safe from any thread (marshalled to the UI thread).
56 57 58 59 |
# File 'lib/roxane/window.rb', line 56 def eval(js) on_ui_thread { FFI.webview_eval(@w, js.to_s) } self end |
#html(markup) ⇒ Object
82 83 84 85 |
# File 'lib/roxane/window.rb', line 82 def html(markup) FFI.webview_set_html(@w, markup.to_s) self end |
#load(url) ⇒ Object
77 78 79 80 |
# File 'lib/roxane/window.rb', line 77 def load(url) FFI.webview_navigate(@w, url.to_s) self end |
#on(op, &block) ⇒ Object
Handle window.roxane.invoke(op, payload). The block runs OFF the UI thread; its return value resolves the JS promise, a raised error rejects it.
44 45 46 47 |
# File 'lib/roxane/window.rb', line 44 def on(op, &block) @handlers[op.to_s] = block self end |
#on_close(&block) ⇒ Object
112 113 114 115 |
# File 'lib/roxane/window.rb', line 112 def on_close(&block) @on_close = block self end |
#run ⇒ Object
Run the native UI loop. Blocks the calling thread until the window closes.
118 119 120 121 122 123 124 |
# File 'lib/roxane/window.rb', line 118 def run FFI.webview_run(@w) ensure @on_close&.call @server&.stop FFI.webview_destroy(@w) end |
#serve(dir) ⇒ Object
Serve a built frontend directory over loopback http and load it.
72 73 74 75 |
# File 'lib/roxane/window.rb', line 72 def serve(dir) @server = StaticServer.new(dir) load(@server.start) end |
#zoom ⇒ Object
Full-content zoom, like a browser's Ctrl+± (1.0 = natural size). Backed by the engine's native zoom (WebKitGTK today); where that binding isn't available the setter no-ops and the reader reports the last requested level. Safe from any thread: scheduled with g_idle_add, which runs on the UI thread as a TOP-LEVEL loop iteration — never nested inside a webview dispatch callback, where a layout-triggering WebKit call can reenter the engine mid-resolve and crash it. Rapid calls coalesce (each application reads the latest requested level; extra idles are harmless).
95 96 97 |
# File 'lib/roxane/window.rb', line 95 def zoom @zoom end |
#zoom=(factor) ⇒ Object
99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/roxane/window.rb', line 99 def zoom=(factor) @zoom = Float(factor) return unless WebKit::AVAILABLE && GLib::AVAILABLE @apply_zoom ||= ::FFI::Function.new(:int, [:pointer]) do |_data| view = FFI.webview_get_native_handle(@w, FFI::NATIVE_HANDLE_BROWSER_CONTROLLER) WebKit.webkit_web_view_set_zoom_level(view, @zoom) unless view.null? 0 # G_SOURCE_REMOVE end @callbacks << @apply_zoom unless @callbacks.include?(@apply_zoom) GLib.g_idle_add(@apply_zoom, nil) end |