Class: Roxane::Window

Inherits:
Object
  • Object
show all
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.expand_path("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

Constructor Details

#initialize(title: "Roxane", size: [800, 600], min_size: nil, resizable: true, debug: false) ⇒ Window

Returns a new instance of Window.

Raises:



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.(...args) -> Promise.



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

#closeObject

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

#runObject

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

#zoomObject

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