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.
- checksums.yaml +7 -0
- data/Cargo.lock +5571 -0
- data/Cargo.toml +3 -0
- data/LICENSE +21 -0
- data/README.md +293 -0
- data/ext/ranma/Cargo.toml +23 -0
- data/ext/ranma/extconf.rb +4 -0
- data/ext/ranma/src/app.rs +231 -0
- data/ext/ranma/src/blit.wgsl +32 -0
- data/ext/ranma/src/clipboard.rs +57 -0
- data/ext/ranma/src/dpi.rs +227 -0
- data/ext/ranma/src/event_loop.rs +145 -0
- data/ext/ranma/src/events.rs +305 -0
- data/ext/ranma/src/hotkey.rs +245 -0
- data/ext/ranma/src/ime.rs +318 -0
- data/ext/ranma/src/lib.rs +42 -0
- data/ext/ranma/src/menu.rs +588 -0
- data/ext/ranma/src/monitor.rs +149 -0
- data/ext/ranma/src/painter.rs +1082 -0
- data/ext/ranma/src/proxy.rs +34 -0
- data/ext/ranma/src/surface.rs +389 -0
- data/ext/ranma/src/theme.rs +20 -0
- data/ext/ranma/src/tray.rs +251 -0
- data/ext/ranma/src/webview.rs +334 -0
- data/ext/ranma/src/window.rs +691 -0
- data/ext/ranma/src/window_store.rs +81 -0
- data/lib/ranma/version.rb +3 -0
- data/lib/ranma.rb +61 -0
- metadata +86 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
use magnus::{method, prelude::*, Error, Ruby};
|
|
2
|
+
use tao::event_loop::EventLoopProxy;
|
|
3
|
+
|
|
4
|
+
#[magnus::wrap(class = "Ranma::EventLoopProxy", free_immediately, size)]
|
|
5
|
+
pub struct RbEventLoopProxy {
|
|
6
|
+
inner: EventLoopProxy<()>,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
impl RbEventLoopProxy {
|
|
10
|
+
pub fn new(proxy: EventLoopProxy<()>) -> Self {
|
|
11
|
+
Self { inner: proxy }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn wake(&self) -> Result<(), Error> {
|
|
15
|
+
self.inner.send_event(()).map_err(|_| {
|
|
16
|
+
Error::new(
|
|
17
|
+
unsafe { Ruby::get_unchecked() }.exception_runtime_error(),
|
|
18
|
+
"Event loop no longer exists",
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub fn inspect(&self) -> String {
|
|
24
|
+
"#<Ranma::EventLoopProxy>".to_string()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn define_proxy_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
29
|
+
let class = module.define_class("EventLoopProxy", ruby.class_object())?;
|
|
30
|
+
class.define_method("wake", method!(RbEventLoopProxy::wake, 0))?;
|
|
31
|
+
class.define_method("inspect", method!(RbEventLoopProxy::inspect, 0))?;
|
|
32
|
+
class.define_method("to_s", method!(RbEventLoopProxy::inspect, 0))?;
|
|
33
|
+
Ok(())
|
|
34
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
use std::cell::RefCell;
|
|
2
|
+
use magnus::{function, method, prelude::*, Error, Ruby};
|
|
3
|
+
use vello::wgpu;
|
|
4
|
+
use vello::{RenderParams, Renderer, RendererOptions, Scene};
|
|
5
|
+
|
|
6
|
+
use crate::window::RbAppWindow;
|
|
7
|
+
use crate::window_store;
|
|
8
|
+
|
|
9
|
+
struct Inner {
|
|
10
|
+
device: wgpu::Device,
|
|
11
|
+
queue: wgpu::Queue,
|
|
12
|
+
surface: wgpu::Surface<'static>,
|
|
13
|
+
config: wgpu::SurfaceConfiguration,
|
|
14
|
+
renderer: Renderer,
|
|
15
|
+
target_texture: wgpu::Texture,
|
|
16
|
+
target_view: wgpu::TextureView,
|
|
17
|
+
blit_pipeline: wgpu::RenderPipeline,
|
|
18
|
+
blit_bind_group_layout: wgpu::BindGroupLayout,
|
|
19
|
+
blit_sampler: wgpu::Sampler,
|
|
20
|
+
width: u32,
|
|
21
|
+
height: u32,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[magnus::wrap(class = "Ranma::GpuSurface", free_immediately, size)]
|
|
25
|
+
pub struct RbGpuSurface {
|
|
26
|
+
inner: RefCell<Inner>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
unsafe impl Send for RbGpuSurface {}
|
|
30
|
+
|
|
31
|
+
fn create_target_texture(
|
|
32
|
+
device: &wgpu::Device,
|
|
33
|
+
width: u32,
|
|
34
|
+
height: u32,
|
|
35
|
+
) -> (wgpu::Texture, wgpu::TextureView) {
|
|
36
|
+
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
|
37
|
+
label: Some("vello_target"),
|
|
38
|
+
size: wgpu::Extent3d {
|
|
39
|
+
width: width.max(1),
|
|
40
|
+
height: height.max(1),
|
|
41
|
+
depth_or_array_layers: 1,
|
|
42
|
+
},
|
|
43
|
+
mip_level_count: 1,
|
|
44
|
+
sample_count: 1,
|
|
45
|
+
dimension: wgpu::TextureDimension::D2,
|
|
46
|
+
format: wgpu::TextureFormat::Rgba8Unorm,
|
|
47
|
+
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
|
|
48
|
+
view_formats: &[],
|
|
49
|
+
});
|
|
50
|
+
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
|
51
|
+
(texture, view)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn create_blit_pipeline(
|
|
55
|
+
device: &wgpu::Device,
|
|
56
|
+
surface_format: wgpu::TextureFormat,
|
|
57
|
+
) -> (
|
|
58
|
+
wgpu::RenderPipeline,
|
|
59
|
+
wgpu::BindGroupLayout,
|
|
60
|
+
wgpu::Sampler,
|
|
61
|
+
) {
|
|
62
|
+
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
63
|
+
label: Some("blit_shader"),
|
|
64
|
+
source: wgpu::ShaderSource::Wgsl(include_str!("blit.wgsl").into()),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
68
|
+
label: Some("blit_bind_group_layout"),
|
|
69
|
+
entries: &[
|
|
70
|
+
wgpu::BindGroupLayoutEntry {
|
|
71
|
+
binding: 0,
|
|
72
|
+
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
73
|
+
ty: wgpu::BindingType::Texture {
|
|
74
|
+
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
|
75
|
+
view_dimension: wgpu::TextureViewDimension::D2,
|
|
76
|
+
multisampled: false,
|
|
77
|
+
},
|
|
78
|
+
count: None,
|
|
79
|
+
},
|
|
80
|
+
wgpu::BindGroupLayoutEntry {
|
|
81
|
+
binding: 1,
|
|
82
|
+
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
83
|
+
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
|
84
|
+
count: None,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
90
|
+
label: Some("blit_pipeline_layout"),
|
|
91
|
+
bind_group_layouts: &[&bind_group_layout],
|
|
92
|
+
push_constant_ranges: &[],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
96
|
+
label: Some("blit_pipeline"),
|
|
97
|
+
layout: Some(&pipeline_layout),
|
|
98
|
+
vertex: wgpu::VertexState {
|
|
99
|
+
module: &shader,
|
|
100
|
+
entry_point: Some("vs_main"),
|
|
101
|
+
buffers: &[],
|
|
102
|
+
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
|
103
|
+
},
|
|
104
|
+
fragment: Some(wgpu::FragmentState {
|
|
105
|
+
module: &shader,
|
|
106
|
+
entry_point: Some("fs_main"),
|
|
107
|
+
targets: &[Some(wgpu::ColorTargetState {
|
|
108
|
+
format: surface_format,
|
|
109
|
+
blend: Some(wgpu::BlendState::REPLACE),
|
|
110
|
+
write_mask: wgpu::ColorWrites::ALL,
|
|
111
|
+
})],
|
|
112
|
+
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
|
113
|
+
}),
|
|
114
|
+
primitive: wgpu::PrimitiveState {
|
|
115
|
+
topology: wgpu::PrimitiveTopology::TriangleList,
|
|
116
|
+
..Default::default()
|
|
117
|
+
},
|
|
118
|
+
depth_stencil: None,
|
|
119
|
+
multisample: wgpu::MultisampleState::default(),
|
|
120
|
+
multiview: None,
|
|
121
|
+
cache: None,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
125
|
+
label: Some("blit_sampler"),
|
|
126
|
+
mag_filter: wgpu::FilterMode::Linear,
|
|
127
|
+
min_filter: wgpu::FilterMode::Linear,
|
|
128
|
+
..Default::default()
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
(pipeline, bind_group_layout, sampler)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
impl RbGpuSurface {
|
|
135
|
+
fn new(window: &RbAppWindow) -> Result<Self, Error> {
|
|
136
|
+
let ruby = unsafe { Ruby::get_unchecked() };
|
|
137
|
+
let window_id = window.raw_window_id();
|
|
138
|
+
|
|
139
|
+
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
|
140
|
+
backends: wgpu::Backends::all(),
|
|
141
|
+
..Default::default()
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let surface = window_store::with_window(&window_id, |tao_window| {
|
|
145
|
+
let target =
|
|
146
|
+
unsafe { wgpu::SurfaceTargetUnsafe::from_window(tao_window) }.map_err(|e| {
|
|
147
|
+
Error::new(
|
|
148
|
+
ruby.exception_runtime_error(),
|
|
149
|
+
format!("Failed to create surface target: {}", e),
|
|
150
|
+
)
|
|
151
|
+
})?;
|
|
152
|
+
let surface = unsafe { instance.create_surface_unsafe(target) }.map_err(|e| {
|
|
153
|
+
Error::new(
|
|
154
|
+
ruby.exception_runtime_error(),
|
|
155
|
+
format!("Failed to create surface: {}", e),
|
|
156
|
+
)
|
|
157
|
+
})?;
|
|
158
|
+
Ok::<_, Error>(surface)
|
|
159
|
+
})
|
|
160
|
+
.ok_or_else(|| {
|
|
161
|
+
Error::new(ruby.exception_runtime_error(), "Window not found")
|
|
162
|
+
})??;
|
|
163
|
+
|
|
164
|
+
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
165
|
+
power_preference: wgpu::PowerPreference::HighPerformance,
|
|
166
|
+
compatible_surface: Some(&surface),
|
|
167
|
+
force_fallback_adapter: false,
|
|
168
|
+
}))
|
|
169
|
+
.map_err(|e| {
|
|
170
|
+
Error::new(
|
|
171
|
+
ruby.exception_runtime_error(),
|
|
172
|
+
format!("No suitable GPU adapter found: {}", e),
|
|
173
|
+
)
|
|
174
|
+
})?;
|
|
175
|
+
|
|
176
|
+
let (device, queue) = pollster::block_on(adapter.request_device(
|
|
177
|
+
&wgpu::DeviceDescriptor {
|
|
178
|
+
label: Some("ranma_device"),
|
|
179
|
+
required_features: wgpu::Features::empty(),
|
|
180
|
+
required_limits: wgpu::Limits::default(),
|
|
181
|
+
memory_hints: wgpu::MemoryHints::default(),
|
|
182
|
+
..Default::default()
|
|
183
|
+
},
|
|
184
|
+
))
|
|
185
|
+
.map_err(|e| {
|
|
186
|
+
Error::new(
|
|
187
|
+
ruby.exception_runtime_error(),
|
|
188
|
+
format!("Failed to create device: {}", e),
|
|
189
|
+
)
|
|
190
|
+
})?;
|
|
191
|
+
|
|
192
|
+
let (width, height) = window_store::with_window(&window_id, |tao_window| {
|
|
193
|
+
let size = tao_window.inner_size();
|
|
194
|
+
(size.width.max(1), size.height.max(1))
|
|
195
|
+
})
|
|
196
|
+
.unwrap_or((800, 600));
|
|
197
|
+
|
|
198
|
+
let surface_caps = surface.get_capabilities(&adapter);
|
|
199
|
+
let surface_format = surface_caps
|
|
200
|
+
.formats
|
|
201
|
+
.iter()
|
|
202
|
+
.find(|f| !f.is_srgb())
|
|
203
|
+
.copied()
|
|
204
|
+
.unwrap_or(surface_caps.formats[0]);
|
|
205
|
+
|
|
206
|
+
let config = wgpu::SurfaceConfiguration {
|
|
207
|
+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
|
208
|
+
format: surface_format,
|
|
209
|
+
width,
|
|
210
|
+
height,
|
|
211
|
+
present_mode: wgpu::PresentMode::AutoVsync,
|
|
212
|
+
alpha_mode: surface_caps.alpha_modes[0],
|
|
213
|
+
view_formats: vec![],
|
|
214
|
+
desired_maximum_frame_latency: 2,
|
|
215
|
+
};
|
|
216
|
+
surface.configure(&device, &config);
|
|
217
|
+
|
|
218
|
+
let renderer = Renderer::new(
|
|
219
|
+
&device,
|
|
220
|
+
RendererOptions {
|
|
221
|
+
use_cpu: false,
|
|
222
|
+
antialiasing_support: vello::AaSupport::area_only(),
|
|
223
|
+
num_init_threads: None,
|
|
224
|
+
pipeline_cache: None,
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
.map_err(|e| {
|
|
228
|
+
Error::new(
|
|
229
|
+
ruby.exception_runtime_error(),
|
|
230
|
+
format!("Failed to create Vello renderer: {:?}", e),
|
|
231
|
+
)
|
|
232
|
+
})?;
|
|
233
|
+
|
|
234
|
+
let (target_texture, target_view) = create_target_texture(&device, width, height);
|
|
235
|
+
let (blit_pipeline, blit_bind_group_layout, blit_sampler) =
|
|
236
|
+
create_blit_pipeline(&device, surface_format);
|
|
237
|
+
|
|
238
|
+
Ok(RbGpuSurface {
|
|
239
|
+
inner: RefCell::new(Inner {
|
|
240
|
+
device,
|
|
241
|
+
queue,
|
|
242
|
+
surface,
|
|
243
|
+
config,
|
|
244
|
+
renderer,
|
|
245
|
+
target_texture,
|
|
246
|
+
target_view,
|
|
247
|
+
blit_pipeline,
|
|
248
|
+
blit_bind_group_layout,
|
|
249
|
+
blit_sampler,
|
|
250
|
+
width,
|
|
251
|
+
height,
|
|
252
|
+
}),
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fn resize(&self, width: u32, height: u32) -> Result<(), Error> {
|
|
257
|
+
let mut inner = self.inner.borrow_mut();
|
|
258
|
+
let width = width.max(1);
|
|
259
|
+
let height = height.max(1);
|
|
260
|
+
if width == inner.width && height == inner.height {
|
|
261
|
+
return Ok(());
|
|
262
|
+
}
|
|
263
|
+
inner.width = width;
|
|
264
|
+
inner.height = height;
|
|
265
|
+
inner.config.width = width;
|
|
266
|
+
inner.config.height = height;
|
|
267
|
+
inner.surface.configure(&inner.device, &inner.config);
|
|
268
|
+
|
|
269
|
+
let (texture, view) = create_target_texture(&inner.device, width, height);
|
|
270
|
+
inner.target_texture = texture;
|
|
271
|
+
inner.target_view = view;
|
|
272
|
+
Ok(())
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fn width(&self) -> u32 {
|
|
276
|
+
self.inner.borrow().width
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn height(&self) -> u32 {
|
|
280
|
+
self.inner.borrow().height
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
impl RbGpuSurface {
|
|
285
|
+
pub fn render_and_present(&self, scene: &Scene) -> Result<(), Error> {
|
|
286
|
+
let ruby = unsafe { Ruby::get_unchecked() };
|
|
287
|
+
let mut inner = self.inner.borrow_mut();
|
|
288
|
+
|
|
289
|
+
let width = inner.width;
|
|
290
|
+
let height = inner.height;
|
|
291
|
+
|
|
292
|
+
let Inner {
|
|
293
|
+
ref device,
|
|
294
|
+
ref queue,
|
|
295
|
+
ref mut renderer,
|
|
296
|
+
ref surface,
|
|
297
|
+
ref target_view,
|
|
298
|
+
ref blit_pipeline,
|
|
299
|
+
ref blit_bind_group_layout,
|
|
300
|
+
ref blit_sampler,
|
|
301
|
+
..
|
|
302
|
+
} = *inner;
|
|
303
|
+
|
|
304
|
+
renderer
|
|
305
|
+
.render_to_texture(
|
|
306
|
+
device,
|
|
307
|
+
queue,
|
|
308
|
+
scene,
|
|
309
|
+
target_view,
|
|
310
|
+
&RenderParams {
|
|
311
|
+
base_color: vello::peniko::Color::TRANSPARENT,
|
|
312
|
+
width,
|
|
313
|
+
height,
|
|
314
|
+
antialiasing_method: vello::AaConfig::Area,
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
.map_err(|e| {
|
|
318
|
+
Error::new(
|
|
319
|
+
ruby.exception_runtime_error(),
|
|
320
|
+
format!("Vello render failed: {:?}", e),
|
|
321
|
+
)
|
|
322
|
+
})?;
|
|
323
|
+
|
|
324
|
+
let output = surface.get_current_texture().map_err(|e| {
|
|
325
|
+
Error::new(
|
|
326
|
+
ruby.exception_runtime_error(),
|
|
327
|
+
format!("Failed to get surface texture: {}", e),
|
|
328
|
+
)
|
|
329
|
+
})?;
|
|
330
|
+
let output_view = output
|
|
331
|
+
.texture
|
|
332
|
+
.create_view(&wgpu::TextureViewDescriptor::default());
|
|
333
|
+
|
|
334
|
+
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
335
|
+
label: Some("blit_bind_group"),
|
|
336
|
+
layout: blit_bind_group_layout,
|
|
337
|
+
entries: &[
|
|
338
|
+
wgpu::BindGroupEntry {
|
|
339
|
+
binding: 0,
|
|
340
|
+
resource: wgpu::BindingResource::TextureView(target_view),
|
|
341
|
+
},
|
|
342
|
+
wgpu::BindGroupEntry {
|
|
343
|
+
binding: 1,
|
|
344
|
+
resource: wgpu::BindingResource::Sampler(blit_sampler),
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
let mut encoder = device
|
|
350
|
+
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
351
|
+
label: Some("blit_encoder"),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
{
|
|
355
|
+
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
356
|
+
label: Some("blit_pass"),
|
|
357
|
+
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
358
|
+
view: &output_view,
|
|
359
|
+
depth_slice: None,
|
|
360
|
+
resolve_target: None,
|
|
361
|
+
ops: wgpu::Operations {
|
|
362
|
+
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
|
363
|
+
store: wgpu::StoreOp::Store,
|
|
364
|
+
},
|
|
365
|
+
})],
|
|
366
|
+
depth_stencil_attachment: None,
|
|
367
|
+
timestamp_writes: None,
|
|
368
|
+
occlusion_query_set: None,
|
|
369
|
+
});
|
|
370
|
+
rpass.set_pipeline(blit_pipeline);
|
|
371
|
+
rpass.set_bind_group(0, &bind_group, &[]);
|
|
372
|
+
rpass.draw(0..3, 0..1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
queue.submit(std::iter::once(encoder.finish()));
|
|
376
|
+
output.present();
|
|
377
|
+
|
|
378
|
+
Ok(())
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
pub fn define_surface_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
383
|
+
let class = module.define_class("GpuSurface", ruby.class_object())?;
|
|
384
|
+
class.define_singleton_method("new", function!(RbGpuSurface::new, 1))?;
|
|
385
|
+
class.define_method("resize", method!(RbGpuSurface::resize, 2))?;
|
|
386
|
+
class.define_method("width", method!(RbGpuSurface::width, 0))?;
|
|
387
|
+
class.define_method("height", method!(RbGpuSurface::height, 0))?;
|
|
388
|
+
Ok(())
|
|
389
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
use magnus::{function, prelude::*, Error, Ruby, Symbol};
|
|
2
|
+
|
|
3
|
+
fn sym(ruby: &Ruby, name: &str) -> Symbol {
|
|
4
|
+
ruby.sym_new(name).into()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
fn ranma_theme_detect() -> Symbol {
|
|
8
|
+
let ruby = unsafe { Ruby::get_unchecked() };
|
|
9
|
+
match dark_light::detect() {
|
|
10
|
+
Ok(dark_light::Mode::Dark) => sym(&ruby, "dark"),
|
|
11
|
+
Ok(dark_light::Mode::Light) => sym(&ruby, "light"),
|
|
12
|
+
_ => sym(&ruby, "unspecified"),
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pub fn define_theme_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
17
|
+
let theme_class = module.define_class("Theme", ruby.class_object())?;
|
|
18
|
+
theme_class.define_singleton_method("detect", function!(ranma_theme_detect, 0))?;
|
|
19
|
+
Ok(())
|
|
20
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
use std::cell::RefCell;
|
|
2
|
+
use std::collections::HashMap;
|
|
3
|
+
use magnus::{function, gc, method, prelude::*, Error, Ruby, RHash, Value};
|
|
4
|
+
use tray_icon::TrayIconBuilder;
|
|
5
|
+
|
|
6
|
+
struct PendingTray {
|
|
7
|
+
id: String,
|
|
8
|
+
tooltip: Option<String>,
|
|
9
|
+
icon: Option<(Vec<u8>, u32, u32)>,
|
|
10
|
+
menu_id: Option<String>,
|
|
11
|
+
visible: bool,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
thread_local! {
|
|
15
|
+
static TRAY_ICONS: RefCell<HashMap<String, tray_icon::TrayIcon>> = RefCell::new(HashMap::new());
|
|
16
|
+
static PENDING_TRAYS: RefCell<Vec<PendingTray>> = RefCell::new(Vec::new());
|
|
17
|
+
static TRAY_EVENT_HANDLER: RefCell<Option<Value>> = const { RefCell::new(None) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn set_tray_event_handler(handler: Value) {
|
|
21
|
+
gc::register_mark_object(handler);
|
|
22
|
+
TRAY_EVENT_HANDLER.with(|cell| {
|
|
23
|
+
*cell.borrow_mut() = Some(handler);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn call_tray_event_handler(_ruby: &Ruby, hash: RHash) {
|
|
28
|
+
TRAY_EVENT_HANDLER.with(|cell| {
|
|
29
|
+
let borrow = cell.borrow();
|
|
30
|
+
if let Some(ref handler) = *borrow {
|
|
31
|
+
let result: Result<Value, _> = handler.funcall("call", (hash,));
|
|
32
|
+
if let Err(e) = result {
|
|
33
|
+
eprintln!("Ranma: Error in tray event handler: {}", e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn has_tray_event_handler() -> bool {
|
|
40
|
+
TRAY_EVENT_HANDLER.with(|cell| cell.borrow().is_some())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn create_pending_trays() {
|
|
44
|
+
PENDING_TRAYS.with(|cell| {
|
|
45
|
+
let pending: Vec<PendingTray> = cell.borrow_mut().drain(..).collect();
|
|
46
|
+
for tray_cfg in pending {
|
|
47
|
+
let mut builder = TrayIconBuilder::new().with_id(tray_cfg.id.clone());
|
|
48
|
+
if let Some(ref tooltip) = tray_cfg.tooltip {
|
|
49
|
+
builder = builder.with_tooltip(tooltip);
|
|
50
|
+
}
|
|
51
|
+
if let Some((rgba, width, height)) = tray_cfg.icon {
|
|
52
|
+
if let Ok(icon) = tray_icon::Icon::from_rgba(rgba, width, height) {
|
|
53
|
+
builder = builder.with_icon(icon);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if let Some(ref menu_id) = tray_cfg.menu_id {
|
|
57
|
+
let menu_id_clone = menu_id.clone();
|
|
58
|
+
crate::menu::with_menu(&menu_id_clone, |menu| {
|
|
59
|
+
// We need to take ownership of builder temporarily
|
|
60
|
+
builder = std::mem::replace(&mut builder, TrayIconBuilder::new())
|
|
61
|
+
.with_menu(Box::new(menu.clone()));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
match builder.build() {
|
|
65
|
+
Ok(tray_icon) => {
|
|
66
|
+
if !tray_cfg.visible {
|
|
67
|
+
let _ = tray_icon.set_visible(false);
|
|
68
|
+
}
|
|
69
|
+
TRAY_ICONS.with(|icons| {
|
|
70
|
+
icons.borrow_mut().insert(tray_cfg.id, tray_icon);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
Err(e) => eprintln!("Ranma: Failed to create tray icon: {}", e),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub fn clear_all() {
|
|
80
|
+
TRAY_ICONS.with(|cell| cell.borrow_mut().clear());
|
|
81
|
+
PENDING_TRAYS.with(|cell| cell.borrow_mut().clear());
|
|
82
|
+
TRAY_EVENT_HANDLER.with(|cell| *cell.borrow_mut() = None);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[magnus::wrap(class = "Ranma::TrayIcon", free_immediately, size)]
|
|
86
|
+
pub struct RbTrayIcon {
|
|
87
|
+
tray_id: String,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn default_icon_rgba() -> (Vec<u8>, u32, u32) {
|
|
91
|
+
// 22x22 simple filled circle icon (suitable for macOS menu bar)
|
|
92
|
+
let size: u32 = 22;
|
|
93
|
+
let mut rgba = vec![0u8; (size * size * 4) as usize];
|
|
94
|
+
let center = size as f64 / 2.0;
|
|
95
|
+
let radius = center - 2.0;
|
|
96
|
+
for y in 0..size {
|
|
97
|
+
for x in 0..size {
|
|
98
|
+
let dx = x as f64 - center;
|
|
99
|
+
let dy = y as f64 - center;
|
|
100
|
+
let dist = (dx * dx + dy * dy).sqrt();
|
|
101
|
+
let idx = ((y * size + x) * 4) as usize;
|
|
102
|
+
if dist <= radius {
|
|
103
|
+
// Dark gray filled circle
|
|
104
|
+
rgba[idx] = 80; // R
|
|
105
|
+
rgba[idx + 1] = 80; // G
|
|
106
|
+
rgba[idx + 2] = 80; // B
|
|
107
|
+
rgba[idx + 3] = 255; // A
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
(rgba, size, size)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
impl RbTrayIcon {
|
|
115
|
+
fn new() -> Self {
|
|
116
|
+
let id = format!("tray_{}", uuid_simple());
|
|
117
|
+
let default_icon = default_icon_rgba();
|
|
118
|
+
PENDING_TRAYS.with(|cell| {
|
|
119
|
+
cell.borrow_mut().push(PendingTray {
|
|
120
|
+
id: id.clone(),
|
|
121
|
+
tooltip: None,
|
|
122
|
+
icon: Some(default_icon),
|
|
123
|
+
menu_id: None,
|
|
124
|
+
visible: true,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
RbTrayIcon { tray_id: id }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn set_tooltip(&self, text: String) {
|
|
131
|
+
let updated = TRAY_ICONS.with(|cell| {
|
|
132
|
+
let icons = cell.borrow();
|
|
133
|
+
if let Some(icon) = icons.get(&self.tray_id) {
|
|
134
|
+
let _ = icon.set_tooltip(Some(&text));
|
|
135
|
+
true
|
|
136
|
+
} else {
|
|
137
|
+
false
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
if !updated {
|
|
141
|
+
PENDING_TRAYS.with(|cell| {
|
|
142
|
+
let mut pending = cell.borrow_mut();
|
|
143
|
+
if let Some(p) = pending.iter_mut().find(|p| p.id == self.tray_id) {
|
|
144
|
+
p.tooltip = Some(text);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn set_icon(&self, rgba: Vec<u8>, width: u32, height: u32) -> Result<(), Error> {
|
|
151
|
+
let ruby = unsafe { Ruby::get_unchecked() };
|
|
152
|
+
let updated = TRAY_ICONS.with(|cell| {
|
|
153
|
+
let icons = cell.borrow();
|
|
154
|
+
if let Some(icon) = icons.get(&self.tray_id) {
|
|
155
|
+
match tray_icon::Icon::from_rgba(rgba.clone(), width, height) {
|
|
156
|
+
Ok(tray_icon_icon) => {
|
|
157
|
+
let _ = icon.set_icon(Some(tray_icon_icon));
|
|
158
|
+
Ok(true)
|
|
159
|
+
}
|
|
160
|
+
Err(e) => Err(Error::new(
|
|
161
|
+
ruby.exception_runtime_error(),
|
|
162
|
+
format!("Invalid icon data: {}", e),
|
|
163
|
+
)),
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
Ok(false)
|
|
167
|
+
}
|
|
168
|
+
})?;
|
|
169
|
+
if !updated {
|
|
170
|
+
PENDING_TRAYS.with(|cell| {
|
|
171
|
+
let mut pending = cell.borrow_mut();
|
|
172
|
+
if let Some(p) = pending.iter_mut().find(|p| p.id == self.tray_id) {
|
|
173
|
+
p.icon = Some((rgba, width, height));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
Ok(())
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn set_menu(&self, menu_id: String) {
|
|
181
|
+
let updated = TRAY_ICONS.with(|cell| {
|
|
182
|
+
let icons = cell.borrow();
|
|
183
|
+
if let Some(icon) = icons.get(&self.tray_id) {
|
|
184
|
+
crate::menu::with_menu(&menu_id, |menu| {
|
|
185
|
+
let _ = icon.set_menu(Some(Box::new(menu.clone())));
|
|
186
|
+
});
|
|
187
|
+
true
|
|
188
|
+
} else {
|
|
189
|
+
false
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
if !updated {
|
|
193
|
+
PENDING_TRAYS.with(|cell| {
|
|
194
|
+
let mut pending = cell.borrow_mut();
|
|
195
|
+
if let Some(p) = pending.iter_mut().find(|p| p.id == self.tray_id) {
|
|
196
|
+
p.menu_id = Some(menu_id);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn set_visible(&self, visible: bool) {
|
|
203
|
+
let updated = TRAY_ICONS.with(|cell| {
|
|
204
|
+
let icons = cell.borrow();
|
|
205
|
+
if let Some(icon) = icons.get(&self.tray_id) {
|
|
206
|
+
let _ = icon.set_visible(visible);
|
|
207
|
+
true
|
|
208
|
+
} else {
|
|
209
|
+
false
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
if !updated {
|
|
213
|
+
PENDING_TRAYS.with(|cell| {
|
|
214
|
+
let mut pending = cell.borrow_mut();
|
|
215
|
+
if let Some(p) = pending.iter_mut().find(|p| p.id == self.tray_id) {
|
|
216
|
+
p.visible = visible;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn id(&self) -> String {
|
|
223
|
+
self.tray_id.clone()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn inspect(&self) -> String {
|
|
227
|
+
format!("#<Ranma::TrayIcon id={}>", self.tray_id)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn uuid_simple() -> String {
|
|
232
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
233
|
+
let t = SystemTime::now()
|
|
234
|
+
.duration_since(UNIX_EPOCH)
|
|
235
|
+
.unwrap_or_default()
|
|
236
|
+
.as_nanos();
|
|
237
|
+
format!("{:x}", t)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
pub fn define_tray_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
241
|
+
let class = module.define_class("TrayIcon", ruby.class_object())?;
|
|
242
|
+
class.define_singleton_method("new", function!(RbTrayIcon::new, 0))?;
|
|
243
|
+
class.define_method("set_tooltip", method!(RbTrayIcon::set_tooltip, 1))?;
|
|
244
|
+
class.define_method("set_icon", method!(RbTrayIcon::set_icon, 3))?;
|
|
245
|
+
class.define_method("set_menu", method!(RbTrayIcon::set_menu, 1))?;
|
|
246
|
+
class.define_method("set_visible", method!(RbTrayIcon::set_visible, 1))?;
|
|
247
|
+
class.define_method("id", method!(RbTrayIcon::id, 0))?;
|
|
248
|
+
class.define_method("inspect", method!(RbTrayIcon::inspect, 0))?;
|
|
249
|
+
class.define_method("to_s", method!(RbTrayIcon::inspect, 0))?;
|
|
250
|
+
Ok(())
|
|
251
|
+
}
|