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.
@@ -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
+ }