ranma 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Cargo.toml ADDED
@@ -0,0 +1,3 @@
1
+ [workspace]
2
+ members = ["ext/ranma"]
3
+ resolver = "2"
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yasushi Itoh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # ranma
2
+
3
+ > Ruby bindings for native windowing, GPU 2D rendering, and WebView — powered by Rust.
4
+ > Wraps [tao](https://github.com/tauri-apps/tao), [wry](https://github.com/tauri-apps/wry),
5
+ > [Vello](https://github.com/linebender/vello), [muda](https://github.com/tauri-apps/muda), and more.
6
+
7
+ [![Gem Version](https://badge.fury.io/rb/ranma.svg)](https://badge.fury.io/rb/ranma)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+ ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey)
10
+
11
+ ## Features
12
+
13
+ - Native windows with full event handling (keyboard, mouse, IME, resize, DPI)
14
+ - Native menu bar (via [muda](https://github.com/tauri-apps/muda))
15
+ - System tray icon with menu (via [tray-icon](https://github.com/tauri-apps/tray-icon))
16
+ - Embedded WebView (WKWebView on macOS, via [wry](https://github.com/tauri-apps/wry))
17
+ - GPU-accelerated 2D graphics — rects, circles, arcs, paths, text, images
18
+ (via [Vello](https://github.com/linebender/vello) + wgpu)
19
+ - Clipboard read/write (via [arboard](https://github.com/1Password/arboard))
20
+ - Global hotkeys (via [global-hotkey](https://github.com/tauri-apps/global-hotkey))
21
+ - System theme detection — dark/light mode (via [dark-light](https://github.com/rust-dark-light/dark-light))
22
+ - Multi-monitor support (monitor enumeration, video modes, exclusive fullscreen)
23
+ - IME preedit support (macOS)
24
+
25
+ ## Requirements
26
+
27
+ - macOS (primary platform; tao also targets Linux and Windows but those are untested)
28
+ - Ruby >= 3.0
29
+ - Rust toolchain (`cargo`) — required to compile the native extension
30
+
31
+ ## Installation
32
+
33
+ Add to your Gemfile:
34
+
35
+ ```ruby
36
+ gem "ranma"
37
+ ```
38
+
39
+ Then run:
40
+
41
+ ```bash
42
+ bundle install
43
+ ```
44
+
45
+ Or install directly:
46
+
47
+ ```bash
48
+ gem install ranma
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```ruby
54
+ require "ranma"
55
+
56
+ Ranma::App.start do
57
+ window = Ranma::AppWindow.new(
58
+ title: "Hello Ranma!",
59
+ inner_size: Ranma::LogicalSize.new(800, 600)
60
+ )
61
+
62
+ window.on_event do |event|
63
+ case event[:type]
64
+ when :close_requested
65
+ Ranma::App.exit
66
+ end
67
+ end
68
+
69
+ window.visible = true
70
+ end
71
+ ```
72
+
73
+ ## API
74
+
75
+ ### App Lifecycle
76
+
77
+ ```ruby
78
+ Ranma::App.start { ... } # Start the event loop; block runs before the loop begins
79
+ Ranma::App.exit # Request application exit
80
+ Ranma::App.request_redraw # Request a redraw of all windows
81
+ ```
82
+
83
+ ### Window (`Ranma::AppWindow`)
84
+
85
+ ```ruby
86
+ window = Ranma::AppWindow.new(
87
+ title: "My App",
88
+ inner_size: Ranma::LogicalSize.new(800, 600)
89
+ )
90
+
91
+ window.on_event { |event| ... } # Per-window event handler
92
+
93
+ window.visible = true
94
+ window.title = "New Title"
95
+ window.resizable = true
96
+ window.decorations = true
97
+ window.inner_size # => Ranma::LogicalSize
98
+ window.outer_position # => Ranma::LogicalPosition
99
+ window.scale_factor # => Float
100
+
101
+ window.set_fullscreen_borderless # Borderless fullscreen
102
+ window.set_fullscreen_exclusive(video_mode) # Exclusive fullscreen
103
+ window.set_ime_position([x, y]) # or Ranma::LogicalPosition.new(x, y)
104
+ window.setup_ime_preedit # Enable IME preedit events (macOS)
105
+ ```
106
+
107
+ ### Events (Hash with `:type` key)
108
+
109
+ | Event type | Additional fields |
110
+ |---|---|
111
+ | `:close_requested` | — |
112
+ | `:resized` | `:width`, `:height` |
113
+ | `:moved` | `:x`, `:y` |
114
+ | `:focused` | `:focused` (bool) |
115
+ | `:keyboard_input` | `:state`, `:key_code`, `:modifiers` |
116
+ | `:received_ime_text` | `:text` |
117
+ | `:ime_preedit` | `:text`, `:cursor_start`, `:cursor_end` |
118
+ | `:cursor_moved` | `:x`, `:y` |
119
+ | `:mouse_input` | `:state`, `:button` |
120
+ | `:mouse_wheel` | `:delta_x`, `:delta_y` |
121
+ | `:scale_factor_changed` | `:scale_factor` |
122
+ | `:theme_changed` | `:theme` (`:dark` or `:light`) |
123
+ | `:redraw_requested` | — |
124
+
125
+ ### Menus
126
+
127
+ ```ruby
128
+ menu = Ranma::Menu.new
129
+ submenu = Ranma::Submenu.new("File", enabled: true)
130
+ item = Ranma::MenuItem.new("Open", enabled: true, accelerator: "CmdOrCtrl+O")
131
+ check = Ranma::CheckMenuItem.new("Show Toolbar", enabled: true, checked: true)
132
+ sep = Ranma::PredefinedMenuItem.separator
133
+ quit = Ranma::PredefinedMenuItem.quit
134
+
135
+ submenu.append(item)
136
+ submenu.append(check)
137
+ submenu.append(sep)
138
+ submenu.append(quit)
139
+ menu.append(submenu)
140
+ menu.init_for_nsapp # macOS: set as the application menu
141
+
142
+ Ranma::App.on_menu_event { |event| puts event[:id] }
143
+ ```
144
+
145
+ ### System Tray
146
+
147
+ ```ruby
148
+ tray = Ranma::TrayIcon.new(icon_path: "icon.png", menu: menu)
149
+
150
+ Ranma::App.on_tray_event { |event| puts event.inspect }
151
+ ```
152
+
153
+ ### WebView
154
+
155
+ ```ruby
156
+ webview = Ranma::WebView.new(window, url: "https://example.com")
157
+ # or
158
+ webview = Ranma::WebView.new(window, html: "<h1>Hello</h1>")
159
+
160
+ webview.load_url("https://example.com")
161
+ webview.load_html("<h1>Hello</h1>")
162
+ webview.evaluate_script("document.title")
163
+ webview.reload
164
+ webview.zoom(1.5)
165
+ webview.set_bounds(0, 0, 800, 600) # positional: (x, y, width, height)
166
+ webview.set_visible(true)
167
+
168
+ webview.on_ipc_message { |msg| puts msg }
169
+ webview.on_navigation { |url| puts url }
170
+ ```
171
+
172
+ ### GPU 2D Graphics
173
+
174
+ ```ruby
175
+ surface = Ranma::GpuSurface.new(window)
176
+ painter = Ranma::Painter.new(surface)
177
+
178
+ window.on_event do |event|
179
+ if event[:type] == :redraw_requested
180
+ painter.clear_all("#ffffff")
181
+
182
+ style = Ranma::PainterStyle.new(
183
+ fill_color: "#3366cc",
184
+ stroke_color: "#000000",
185
+ stroke_width: 2.0,
186
+ font_size: 24.0,
187
+ border_radius: 8.0
188
+ )
189
+ painter.style(style)
190
+
191
+ painter.fill_rect(10, 10, 200, 100)
192
+ painter.stroke_rect(10, 10, 200, 100)
193
+ painter.fill_circle(150, 150, 50)
194
+ painter.fill_text("Hello!", 50, 50, nil) # 4th arg: max_width (Float or nil)
195
+
196
+ painter.save
197
+ painter.translate(100, 100)
198
+ painter.scale(2.0, 2.0)
199
+ painter.fill_rect(0, 0, 50, 50)
200
+ painter.restore
201
+
202
+ painter.flush
203
+ end
204
+ end
205
+ ```
206
+
207
+ #### Available drawing methods
208
+
209
+ | Method | Description |
210
+ |---|---|
211
+ | `clear_all(color_hex = nil)` | Fill entire surface with a hex color (e.g. `"#ffffff"`); omit to clear to transparent |
212
+ | `style(painter_style)` | Set the active `Ranma::PainterStyle` for subsequent draw calls |
213
+ | `fill_rect(x, y, w, h)` | Filled rectangle (respects `border_radius` from active style) |
214
+ | `stroke_rect(x, y, w, h)` | Stroked rectangle |
215
+ | `fill_circle(cx, cy, r)` | Filled circle |
216
+ | `fill_arc(cx, cy, r, start_angle, end_angle)` | Filled arc |
217
+ | `stroke_arc(cx, cy, r, start_angle, end_angle)` | Stroked arc |
218
+ | `fill_triangle(x1,y1, x2,y2, x3,y3)` | Filled triangle |
219
+ | `fill_text(text, x, y, max_width)` | Text rendering; `max_width` is a Float or `nil` |
220
+ | `measure_text(text)` | Returns `Ranma::PainterFontMetrics` |
221
+ | `draw_image(path, x, y, w, h)` | Draw image from file |
222
+ | `measure_image(path)` | Returns `[width, height]` |
223
+ | `begin_path / path_move_to / path_line_to / fill_path` | Path drawing |
224
+ | `save / restore` | Save/restore transform+clip state |
225
+ | `translate(dx, dy) / scale(sx, sy)` | Transform |
226
+ | `clip(x, y, w, h)` | Clip rectangle |
227
+ | `flush` | Submit frame to GPU |
228
+
229
+ ### Clipboard
230
+
231
+ ```ruby
232
+ Ranma::Clipboard.set_text("Hello, world!")
233
+ text = Ranma::Clipboard.get_text
234
+ Ranma::Clipboard.clear
235
+ ```
236
+
237
+ ### HotKey
238
+
239
+ ```ruby
240
+ hotkey = Ranma::HotKey.new(modifiers: [:ctrl, :shift], key: :a)
241
+ hotkey.register { puts "Ctrl+Shift+A pressed!" }
242
+ hotkey.unregister
243
+ ```
244
+
245
+ ### Theme
246
+
247
+ ```ruby
248
+ theme = Ranma::Theme.detect # => :dark | :light | :unspecified
249
+ ```
250
+
251
+ ### DPI Types
252
+
253
+ ```ruby
254
+ Ranma::LogicalSize.new(width, height)
255
+ Ranma::PhysicalSize.new(width, height)
256
+ Ranma::LogicalPosition.new(x, y)
257
+ Ranma::PhysicalPosition.new(x, y)
258
+ ```
259
+
260
+ ## Examples
261
+
262
+ The `examples/` directory contains runnable demonstrations:
263
+
264
+ | File | Description |
265
+ |---|---|
266
+ | `basic_window.rb` | Minimal window using the high-level API |
267
+ | `event_handling.rb` | Per-window event handling |
268
+ | `low_level.rb` | Low-level `Ranma.run` API |
269
+ | `menu_example.rb` | Native menu bar with menu events |
270
+ | `full_featured.rb` | Full demo: menus, tray, IME, monitors |
271
+ | `clipboard_example.rb` | Clipboard read/write operations |
272
+ | `hotkey_example.rb` | Global hotkey registration |
273
+ | `webview_example.rb` | Embedded WebView (wry) |
274
+ | `painter_example.rb` | GPU-accelerated 2D drawing (Vello + Painter) |
275
+
276
+ Run any example with:
277
+
278
+ ```bash
279
+ bundle exec ruby examples/basic_window.rb
280
+ ```
281
+
282
+ ## Building from Source
283
+
284
+ ```bash
285
+ git clone https://github.com/i2y/ranma
286
+ cd ranma
287
+ bundle install
288
+ bundle exec rake compile
289
+ ```
290
+
291
+ ## License
292
+
293
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,23 @@
1
+ [package]
2
+ name = "ranma"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ publish = false
6
+
7
+ [lib]
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ magnus = "0.8"
12
+ rb-sys = "*"
13
+ tao = "0.34"
14
+ muda = "0.17"
15
+ tray-icon = "0.21"
16
+ dark-light = "2.0"
17
+ arboard = "3.6"
18
+ global-hotkey = "0.7"
19
+ wry = { version = "0.54", features = ["devtools"] }
20
+ vello = "0.7"
21
+ parley = "0.4"
22
+ image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
23
+ pollster = "0.4"
@@ -0,0 +1,4 @@
1
+ require "mkmf"
2
+ require "rb_sys/mkmf"
3
+
4
+ create_rust_makefile("ranma/ranma")
@@ -0,0 +1,231 @@
1
+ use std::cell::RefCell;
2
+ use std::sync::OnceLock;
3
+ use magnus::{function, gc, prelude::*, block::Proc, Error, Ruby, Symbol, Value};
4
+ use tao::event::{Event, StartCause};
5
+ use tao::event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget};
6
+
7
+ #[cfg(target_os = "macos")]
8
+ use tao::platform::macos::{ActivationPolicy, EventLoopExtMacOS};
9
+
10
+ use crate::events::convert_window_event;
11
+ use crate::window_store;
12
+
13
+ #[derive(Debug)]
14
+ pub enum UserEvent {
15
+ Exit,
16
+ Redraw,
17
+ MenuEvent(muda::MenuEvent),
18
+ TrayEvent(tray_icon::TrayIconEvent),
19
+ }
20
+
21
+ static GLOBAL_PROXY: OnceLock<EventLoopProxy<UserEvent>> = OnceLock::new();
22
+
23
+ fn sym(ruby: &Ruby, name: &str) -> Symbol {
24
+ ruby.sym_new(name).into()
25
+ }
26
+
27
+ thread_local! {
28
+ static EVENT_LOOP_TARGET: RefCell<Option<*const EventLoopWindowTarget<UserEvent>>> = const { RefCell::new(None) };
29
+ static EXIT_PROXY: RefCell<Option<tao::event_loop::EventLoopProxy<UserEvent>>> = const { RefCell::new(None) };
30
+ static APP_MENU_HANDLER: RefCell<Option<Value>> = const { RefCell::new(None) };
31
+ }
32
+
33
+ pub fn with_event_loop_target<F, R>(f: F) -> Option<R>
34
+ where
35
+ F: FnOnce(&EventLoopWindowTarget<UserEvent>) -> R,
36
+ {
37
+ EVENT_LOOP_TARGET.with(|cell| {
38
+ let borrow = cell.borrow();
39
+ borrow.map(|ptr| {
40
+ let target = unsafe { &*ptr };
41
+ f(target)
42
+ })
43
+ })
44
+ }
45
+
46
+ fn ranma_app_start(callback: Proc) -> Result<(), Error> {
47
+ let mut event_loop = EventLoopBuilder::with_user_event().build();
48
+
49
+ #[cfg(target_os = "macos")]
50
+ {
51
+ event_loop.set_activation_policy(ActivationPolicy::Regular);
52
+ event_loop.set_activate_ignoring_other_apps(true);
53
+ }
54
+
55
+ let proxy = event_loop.create_proxy();
56
+ EXIT_PROXY.with(|cell| {
57
+ *cell.borrow_mut() = Some(proxy);
58
+ });
59
+
60
+ let _ = GLOBAL_PROXY.set(event_loop.create_proxy());
61
+
62
+ // Set up muda menu event handler to forward events via the event loop proxy
63
+ let menu_proxy = event_loop.create_proxy();
64
+ muda::MenuEvent::set_event_handler(Some(move |event| {
65
+ let _ = menu_proxy.send_event(UserEvent::MenuEvent(event));
66
+ }));
67
+
68
+ // Set up tray-icon event handler to forward events via the event loop proxy
69
+ let tray_proxy = event_loop.create_proxy();
70
+ tray_icon::TrayIconEvent::set_event_handler(Some(move |event| {
71
+ let _ = tray_proxy.send_event(UserEvent::TrayEvent(event));
72
+ }));
73
+
74
+ // Set up the event loop target BEFORE calling the callback,
75
+ // so that AppWindow.new can use it to create windows.
76
+ EVENT_LOOP_TARGET.with(|cell| {
77
+ let target: &EventLoopWindowTarget<UserEvent> = &event_loop;
78
+ *cell.borrow_mut() = Some(target as *const _);
79
+ });
80
+
81
+ // Initialize hotkey manager before setup callback
82
+ crate::hotkey::init_manager();
83
+
84
+ // Call the setup callback BEFORE run().
85
+ let result = callback.call::<_, Value>(());
86
+ if let Err(e) = result {
87
+ eprintln!("Ranma: Error in App.start callback: {}", e);
88
+ return Err(e);
89
+ }
90
+
91
+ event_loop.run(move |event, event_loop_window_target, control_flow| {
92
+ *control_flow = ControlFlow::Wait;
93
+
94
+ // Allow Ruby background threads (e.g. image downloaders) to run.
95
+ // Without this, the GVL is held continuously and other threads starve.
96
+ unsafe { rb_sys::rb_thread_schedule(); }
97
+
98
+ EVENT_LOOP_TARGET.with(|cell| {
99
+ *cell.borrow_mut() = Some(event_loop_window_target as *const _);
100
+ });
101
+
102
+ let ruby = unsafe { Ruby::get_unchecked() };
103
+
104
+ match event {
105
+ Event::NewEvents(StartCause::Init) => {
106
+ crate::tray::create_pending_trays();
107
+ }
108
+ Event::NewEvents(_) => {
109
+ while let Ok(event) = global_hotkey::GlobalHotKeyEvent::receiver().try_recv() {
110
+ crate::hotkey::dispatch_hotkey_event(event.id());
111
+ }
112
+ crate::ime::dispatch_pending_preedit();
113
+ }
114
+ Event::UserEvent(UserEvent::Exit) => {
115
+ *control_flow = ControlFlow::Exit;
116
+ }
117
+ Event::UserEvent(UserEvent::Redraw) => {
118
+ window_store::request_redraw_all();
119
+ }
120
+ Event::UserEvent(UserEvent::MenuEvent(e)) => {
121
+ APP_MENU_HANDLER.with(|cell| {
122
+ let borrow = cell.borrow();
123
+ if let Some(ref handler) = *borrow {
124
+ let hash = ruby.hash_new();
125
+ let _ = hash.aset(sym(&ruby, "type"), sym(&ruby, "menu_event"));
126
+ let _ = hash.aset(sym(&ruby, "menu_id"), e.id.0.clone());
127
+ let result: Result<Value, _> = handler.funcall("call", (hash,));
128
+ if let Err(err) = result {
129
+ eprintln!("Ranma: Error in menu event handler: {}", err);
130
+ }
131
+ }
132
+ });
133
+ }
134
+ Event::UserEvent(UserEvent::TrayEvent(e)) => {
135
+ if let tray_icon::TrayIconEvent::Click {
136
+ position,
137
+ button,
138
+ button_state: tray_icon::MouseButtonState::Up,
139
+ ..
140
+ } = e
141
+ {
142
+ if crate::tray::has_tray_event_handler() {
143
+ let hash = ruby.hash_new();
144
+ let _ = hash.aset(sym(&ruby, "type"), sym(&ruby, "tray_click"));
145
+ let _ = hash.aset(sym(&ruby, "x"), position.x);
146
+ let _ = hash.aset(sym(&ruby, "y"), position.y);
147
+ let btn_sym = match button {
148
+ tray_icon::MouseButton::Left => "left",
149
+ tray_icon::MouseButton::Right => "right",
150
+ tray_icon::MouseButton::Middle => "middle",
151
+ };
152
+ let _ = hash.aset(sym(&ruby, "button"), sym(&ruby, btn_sym));
153
+ crate::tray::call_tray_event_handler(&ruby, hash);
154
+ }
155
+ }
156
+ }
157
+ Event::WindowEvent {
158
+ event: ref win_event,
159
+ window_id,
160
+ ..
161
+ } => {
162
+ let event_hash = convert_window_event(&ruby, win_event);
163
+ window_store::dispatch_event(&ruby, &window_id, event_hash);
164
+ }
165
+ Event::RedrawRequested(window_id) => {
166
+ let hash = ruby.hash_new();
167
+ let type_key: Symbol = ruby.sym_new("type").into();
168
+ let type_val: Symbol = ruby.sym_new("redraw_requested").into();
169
+ let _ = hash.aset(type_key, type_val);
170
+ window_store::dispatch_event(&ruby, &window_id, hash);
171
+ }
172
+ Event::LoopDestroyed => {
173
+ EVENT_LOOP_TARGET.with(|cell| {
174
+ *cell.borrow_mut() = None;
175
+ });
176
+ EXIT_PROXY.with(|cell| {
177
+ *cell.borrow_mut() = None;
178
+ });
179
+ APP_MENU_HANDLER.with(|cell| {
180
+ *cell.borrow_mut() = None;
181
+ });
182
+ window_store::clear_all();
183
+ crate::menu::clear_all();
184
+ crate::tray::clear_all();
185
+ crate::hotkey::clear_all();
186
+ crate::webview::clear_all();
187
+ crate::monitor::clear_video_modes();
188
+ }
189
+ _ => {}
190
+ }
191
+ });
192
+ }
193
+
194
+ fn ranma_app_exit() -> Result<(), Error> {
195
+ EXIT_PROXY.with(|cell| {
196
+ if let Some(proxy) = cell.borrow().as_ref() {
197
+ let _ = proxy.send_event(UserEvent::Exit);
198
+ }
199
+ });
200
+ Ok(())
201
+ }
202
+
203
+ fn ranma_app_request_redraw() -> Result<(), Error> {
204
+ if let Some(proxy) = GLOBAL_PROXY.get() {
205
+ let _ = proxy.send_event(UserEvent::Redraw);
206
+ }
207
+ Ok(())
208
+ }
209
+
210
+ fn ranma_app_on_menu_event(handler: Value) -> Result<(), Error> {
211
+ gc::register_mark_object(handler);
212
+ APP_MENU_HANDLER.with(|cell| {
213
+ *cell.borrow_mut() = Some(handler);
214
+ });
215
+ Ok(())
216
+ }
217
+
218
+ fn ranma_app_on_tray_event(handler: Value) -> Result<(), Error> {
219
+ crate::tray::set_tray_event_handler(handler);
220
+ Ok(())
221
+ }
222
+
223
+ pub fn define_app(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
224
+ let app_class = module.define_class("App", ruby.class_object())?;
225
+ app_class.define_singleton_method("start", function!(ranma_app_start, 1))?;
226
+ app_class.define_singleton_method("exit", function!(ranma_app_exit, 0))?;
227
+ app_class.define_singleton_method("request_redraw", function!(ranma_app_request_redraw, 0))?;
228
+ app_class.define_singleton_method("on_menu_event", function!(ranma_app_on_menu_event, 1))?;
229
+ app_class.define_singleton_method("on_tray_event", function!(ranma_app_on_tray_event, 1))?;
230
+ Ok(())
231
+ }
@@ -0,0 +1,32 @@
1
+ // Fullscreen triangle blit shader:
2
+ // Copies Vello render_to_texture output (Rgba8Unorm) to surface texture.
3
+
4
+ @group(0) @binding(0)
5
+ var src_texture: texture_2d<f32>;
6
+ @group(0) @binding(1)
7
+ var src_sampler: sampler;
8
+
9
+ struct VertexOutput {
10
+ @builtin(position) position: vec4<f32>,
11
+ @location(0) uv: vec2<f32>,
12
+ };
13
+
14
+ @vertex
15
+ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
16
+ // Fullscreen triangle: 3 vertices that cover the entire clip space [-1,1]
17
+ // vertex 0: (-1, -1), vertex 1: (3, -1), vertex 2: (-1, 3)
18
+ var out: VertexOutput;
19
+ let x = f32(i32(vertex_index & 1u)) * 4.0 - 1.0;
20
+ let y = f32(i32(vertex_index >> 1u)) * 4.0 - 1.0;
21
+ out.position = vec4<f32>(x, y, 0.0, 1.0);
22
+ out.uv = vec2<f32>(
23
+ (x + 1.0) * 0.5,
24
+ (1.0 - y) * 0.5,
25
+ );
26
+ return out;
27
+ }
28
+
29
+ @fragment
30
+ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
31
+ return textureSample(src_texture, src_sampler, in.uv);
32
+ }
@@ -0,0 +1,57 @@
1
+ use magnus::{function, prelude::*, Error, Ruby};
2
+
3
+ fn ranma_clipboard_get_text() -> Result<String, Error> {
4
+ let ruby = unsafe { Ruby::get_unchecked() };
5
+ let mut clipboard = arboard::Clipboard::new().map_err(|e| {
6
+ Error::new(
7
+ ruby.exception_runtime_error(),
8
+ format!("Failed to access clipboard: {}", e),
9
+ )
10
+ })?;
11
+ clipboard.get_text().map_err(|e| {
12
+ Error::new(
13
+ ruby.exception_runtime_error(),
14
+ format!("Failed to get clipboard text: {}", e),
15
+ )
16
+ })
17
+ }
18
+
19
+ fn ranma_clipboard_set_text(text: String) -> Result<(), Error> {
20
+ let ruby = unsafe { Ruby::get_unchecked() };
21
+ let mut clipboard = arboard::Clipboard::new().map_err(|e| {
22
+ Error::new(
23
+ ruby.exception_runtime_error(),
24
+ format!("Failed to access clipboard: {}", e),
25
+ )
26
+ })?;
27
+ clipboard.set_text(text).map_err(|e| {
28
+ Error::new(
29
+ ruby.exception_runtime_error(),
30
+ format!("Failed to set clipboard text: {}", e),
31
+ )
32
+ })
33
+ }
34
+
35
+ fn ranma_clipboard_clear() -> Result<(), Error> {
36
+ let ruby = unsafe { Ruby::get_unchecked() };
37
+ let mut clipboard = arboard::Clipboard::new().map_err(|e| {
38
+ Error::new(
39
+ ruby.exception_runtime_error(),
40
+ format!("Failed to access clipboard: {}", e),
41
+ )
42
+ })?;
43
+ clipboard.clear().map_err(|e| {
44
+ Error::new(
45
+ ruby.exception_runtime_error(),
46
+ format!("Failed to clear clipboard: {}", e),
47
+ )
48
+ })
49
+ }
50
+
51
+ pub fn define_clipboard_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
52
+ let class = module.define_class("Clipboard", ruby.class_object())?;
53
+ class.define_singleton_method("get_text", function!(ranma_clipboard_get_text, 0))?;
54
+ class.define_singleton_method("set_text", function!(ranma_clipboard_set_text, 1))?;
55
+ class.define_singleton_method("clear", function!(ranma_clipboard_clear, 0))?;
56
+ Ok(())
57
+ }