summaryrefslogtreecommitdiffhomepage
path: root/mrbgems/rmlui/mrblib/rmlui.rb
blob: f4fe4eb53da7e0705403fa9aeea86d77894df1a8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# Friendly Ruby surface for RmlUi, built on the low-level Rml._* primitives.
# Implements the subset of docs/API_SPEC_RMLUI.md needed for the first milestone:
# init, fonts, a Context, loading + showing a static document, and the per-frame
# update/render/input cycle.

module Rml
  class << self
    # Must be called AFTER Rl.init_window (needs the GL context).
    def init
      _init
    end

    def shutdown
      _shutdown
    end

    def load_font(path, fallback: false)
      _load_font(path.to_s, fallback)
    end
  end

  # A node in the RML document tree. Wraps a native Element* (non-owning).
  # (API_SPEC_RMLUI 6)
  class Element
    attr_reader :ptr
    def initialize(ptr); @ptr = ptr; end

    # attributes
    def [](name)        = Rml._el_get_attribute(@ptr, name.to_s)
    def []=(name, v); Rml._el_set_attribute(@ptr, name.to_s, v.to_s); end
    def attribute(name) = Rml._el_get_attribute(@ptr, name.to_s)
    def set_attribute(name, v); Rml._el_set_attribute(@ptr, name.to_s, v.to_s); self; end
    def has_attribute?(name) = Rml._el_has_attribute(@ptr, name.to_s)
    def remove_attribute(name); Rml._el_remove_attribute(@ptr, name.to_s); self; end

    # identity / content
    def id          = Rml._el_get_id(@ptr)
    def id=(v); Rml._el_set_id(@ptr, v.to_s); end
    def tag_name    = Rml._el_tag(@ptr)
    def inner_rml   = Rml._el_get_inner_rml(@ptr)
    def inner_rml=(v); Rml._el_set_inner_rml(@ptr, v.to_s); end
    alias_method :text, :inner_rml
    def text=(v); Rml._el_set_inner_rml(@ptr, v.to_s); end

    # classes / style properties
    def set_class(name, on); Rml._el_set_class(@ptr, name.to_s, on); self; end
    def add_class(name);    set_class(name, true); end
    def remove_class(name); set_class(name, false); end
    def class_set?(name) = Rml._el_is_class_set(@ptr, name.to_s)
    def set_property(name, val); Rml._el_set_property(@ptr, name.to_s, val.to_s); self; end
    def property(name)  = Rml._el_get_property(@ptr, name.to_s)
    def remove_property(name); Rml._el_remove_property(@ptr, name.to_s); self; end

    # actions
    def focus; Rml._el_focus(@ptr); self; end
    def blur;  Rml._el_blur(@ptr);  self; end
    def click; Rml._el_click(@ptr); self; end
    def scroll_into_view(align_top = true); Rml._el_scroll_into_view(@ptr, align_top); self; end
    def visible? = Rml._el_is_visible(@ptr)
    def select_all; Rml._el_select(@ptr); self; end
    def set_selection_range(start, finish); Rml._el_set_selection_range(@ptr, start, finish); self; end
    def caret_end; v = self["value"].to_s; set_selection_range(v.length, v.length); self; end

    # traversal / queries (return Element / Array<Element> / nil)
    def element(id)            = Rml._el_get_element_by_id(@ptr, id.to_s)
    alias_method :get_element_by_id, :element
    def query_selector(sel)    = Rml._el_query_selector(@ptr, sel.to_s)
    def query_selector_all(sel) = Rml._el_query_selector_all(@ptr, sel.to_s)
    def elements_by_tag(tag)   = Rml._el_get_elements_by_tag(@ptr, tag.to_s)
    def parent          = Rml._el_parent(@ptr)
    def child_count     = Rml._el_num_children(@ptr)
    def child(i)        = Rml._el_child(@ptr, i)
    def children        = (0...child_count).map { |i| child(i) }
    def owner_document  = Rml._el_owner_document(@ptr)

    # geometry
    def client_width  = Rml._el_client_width(@ptr)
    def client_height = Rml._el_client_height(@ptr)
    def offset_left   = Rml._el_offset_left(@ptr)
    def offset_top    = Rml._el_offset_top(@ptr)
    def absolute_left = Rml._el_absolute_left(@ptr)
    def absolute_top  = Rml._el_absolute_top(@ptr)

    # events: el.on(:click) { |event| ... }
    def on(type, &block); Rml._el_add_event_listener(@ptr, type.to_s, &block); self; end
  end

  # An event delivered to an Element#on listener. (API_SPEC_RMLUI 5)
  class Event
    def initialize(ptr); @ptr = ptr; end
    def type    = Rml._ev_type(@ptr)
    def target  = Rml._ev_target(@ptr)
    def current = Rml._ev_current(@ptr)
    def stop_propagation;           Rml._ev_stop_propagation(@ptr); end
    def stop_immediate_propagation; Rml._ev_stop_immediate(@ptr); end
    def [](key)        = Rml._ev_param_float(@ptr, key.to_s)
    def param(key)     = Rml._ev_param_float(@ptr, key.to_s)
    def param_str(key) = Rml._ev_param_str(@ptr, key.to_s)
    def mouse_x = param("mouse_x")
    def mouse_y = param("mouse_y")
  end

  # An ElementDocument: an Element with show/hide/title/etc.
  class Document < Element
    def show; Rml._document_show(@ptr); self; end
    def hide; Rml._document_hide(@ptr); self; end
    def close; Rml._doc_close(@ptr); self; end
    def title      = Rml._doc_title(@ptr)
    def title=(t); Rml._doc_set_title(@ptr, t.to_s); end
    def pull_to_front; Rml._doc_pull_to_front(@ptr); self; end
    def push_to_back;  Rml._doc_push_to_back(@ptr);  self; end
  end

  # MVC data model: binds Ruby state to {{vars}} / data-* attributes in RML.
  # (API_SPEC_RMLUI 4)
  class DataModel
    def initialize(ctx_ptr, name)
      @getters = {}
      @values  = {}
      @events  = {}
      @ptr = Rml._data_model_create(ctx_ptr, name.to_s, self)
    end

    # one-way computed view: m.bind(:hp) { player.hp }
    def bind(name, &getter)
      @getters[name.to_s] = getter
      Rml._data_model_bind_get(@ptr, name.to_s)
      self
    end

    # two-way scalar: m.value(:volume, 0.5)
    def value(name, initial)
      @values[name.to_s] = initial
      Rml._data_model_bind_scalar(@ptr, name.to_s)
      self
    end

    # controller callback: m.event(:reset) { ... }  (rml: data-event-click="reset()")
    def event(name, &blk)
      @events[name.to_s] = blk
      Rml._data_model_bind_event(@ptr, name.to_s)
      self
    end

    def finish
      Rml._data_model_finish(@ptr)
      self
    end

    # notify the view that bound variables changed (DataModelHandle::DirtyVariable)
    def dirty(*names)
      names.each { |n| Rml._data_model_dirty(@ptr, n.to_s) }
      self
    end

    def dirty_all
      Rml._data_model_dirty_all(@ptr)
      self
    end

    def [](name)
      @values[name.to_s]
    end

    def []=(name, v)
      @values[name.to_s] = v
      Rml._data_model_dirty(@ptr, name.to_s)
    end

    # --- called from the C++ bridge ---
    def __get(name)
      @getters.key?(name) ? @getters[name].call : @values[name]
    end

    def __set(name, v)
      @values[name] = v
    end

    def __event(name)
      blk = @events[name]
      blk.call if blk
    end
  end

  class Context
    def initialize(name, width: nil, height: nil)
      width  ||= Rl.screen_width
      height ||= Rl.screen_height
      @ptr = Rml._create_context(name.to_s, width, height)
      raise "failed to create RmlUi context #{name.inspect}" if @ptr.nil?
    end

    def dimensions=(vec2)
      Rml._context_set_dimensions(@ptr, vec2.x, vec2.y)
    end

    def resize(width, height)
      Rml._context_set_dimensions(@ptr, width, height)
    end

    # Create + configure a data model. Must be called BEFORE load_document so the
    # document can bind to it by name. (API_SPEC_RMLUI 4.1)
    def data_model(name)
      m = DataModel.new(@ptr, name)
      yield m if block_given?
      m.finish
      m
    end

    def load_document(path)
      ptr = Rml._context_load_document(@ptr, path.to_s)
      raise "failed to load document #{path}" if ptr.nil?
      doc = Document.new(ptr)
      yield doc if block_given?
      doc
    end

    # Look up an already-loaded document by its id/source. Returns a bare
    # Element wrapper (use load_document's return value for Document methods).
    def document(id)   = Rml._context_get_document(@ptr, id.to_s)
    def num_documents  = Rml._context_num_documents(@ptr)

    def process_input
      Rml._context_process_input(@ptr)
    end

    def update
      Rml._context_update(@ptr)
    end

    def render
      Rml._context_render(@ptr)
    end

    # Block helper: process_input before, update+render after (API_SPEC_RMLUI 2).
    def frame
      process_input
      yield
    ensure
      update
      render
    end
  end
end