From b799c4291c469a3d906c2c80ebea473d8f095c7e Mon Sep 17 00:00:00 2001 From: ljlvink Date: Sun, 22 Mar 2026 23:40:19 +0800 Subject: [PATCH 1/3] chore: cargo fmt and clippy --- examples/ime_test.rs | 471 ++++++++++++++++++++++++++++++--------- examples/mouse_cursor.rs | 10 +- src/event.rs | 2 +- src/native/linux_x11.rs | 3 +- src/native/windows.rs | 80 ++++--- 5 files changed, 428 insertions(+), 138 deletions(-) diff --git a/examples/ime_test.rs b/examples/ime_test.rs index de760021f..588861b17 100644 --- a/examples/ime_test.rs +++ b/examples/ime_test.rs @@ -24,8 +24,8 @@ const FONT_PATH: &str = "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc" // ============================================================================ struct GlyphInfo { - uv: [f32; 4], // x, y, w, h in UV coords - size: [f32; 2], // width, height in pixels + uv: [f32; 4], // x, y, w, h in UV coords + size: [f32; 2], // width, height in pixels offset: [f32; 2], // xmin, ymin advance: f32, } @@ -48,25 +48,41 @@ impl TextRenderer { let font_data = std::fs::read(FONT_PATH).expect("Failed to load font file"); let font = Font::from_bytes(font_data.as_slice(), FontSettings::default()) .expect("Failed to parse font"); - + let atlas_size = 1024u32; let atlas_data = vec![0u8; (atlas_size * atlas_size * 4) as usize]; let atlas = ctx.new_texture_from_rgba8(atlas_size as u16, atlas_size as u16, &atlas_data); Self { - font, font_size, cache: HashMap::new(), atlas, atlas_data, - atlas_size, cursor_x: 0, cursor_y: 0, row_h: 0, dirty: false, + font, + font_size, + cache: HashMap::new(), + atlas, + atlas_data, + atlas_size, + cursor_x: 0, + cursor_y: 0, + row_h: 0, + dirty: false, } } fn cache_char(&mut self, ch: char) { - if self.cache.contains_key(&ch) { return; } - + if self.cache.contains_key(&ch) { + return; + } + let (m, bmp) = self.font.rasterize(ch, self.font_size); if m.width == 0 || m.height == 0 { - self.cache.insert(ch, GlyphInfo { - uv: [0.0; 4], size: [0.0; 2], offset: [0.0; 2], advance: m.advance_width, - }); + self.cache.insert( + ch, + GlyphInfo { + uv: [0.0; 4], + size: [0.0; 2], + offset: [0.0; 2], + advance: m.advance_width, + }, + ); return; } @@ -87,19 +103,26 @@ impl TextRenderer { let dx = self.cursor_x + x as u32; let dy = self.cursor_y + y as u32; let dst = ((dy * self.atlas_size + dx) * 4) as usize; - self.atlas_data[dst..dst+3].copy_from_slice(&[255, 255, 255]); + self.atlas_data[dst..dst + 3].copy_from_slice(&[255, 255, 255]); self.atlas_data[dst + 3] = bmp[src]; } } let s = self.atlas_size as f32; - self.cache.insert(ch, GlyphInfo { - uv: [self.cursor_x as f32 / s, self.cursor_y as f32 / s, - m.width as f32 / s, m.height as f32 / s], - size: [m.width as f32, m.height as f32], - offset: [m.xmin as f32, m.ymin as f32], - advance: m.advance_width, - }); + self.cache.insert( + ch, + GlyphInfo { + uv: [ + self.cursor_x as f32 / s, + self.cursor_y as f32 / s, + m.width as f32 / s, + m.height as f32 / s, + ], + size: [m.width as f32, m.height as f32], + offset: [m.xmin as f32, m.ymin as f32], + advance: m.advance_width, + }, + ); self.cursor_x += m.width as u32 + 1; self.row_h = self.row_h.max(m.height as u32); @@ -115,10 +138,12 @@ impl TextRenderer { } fn measure(&mut self, text: &str) -> f32 { - text.chars().map(|c| { - self.cache_char(c); - self.cache.get(&c).map(|g| g.advance).unwrap_or(0.0) - }).sum() + text.chars() + .map(|c| { + self.cache_char(c); + self.cache.get(&c).map(|g| g.advance).unwrap_or(0.0) + }) + .sum() } } @@ -127,7 +152,10 @@ impl TextRenderer { // ============================================================================ struct InputBox { - x: f32, y: f32, w: f32, h: f32, + x: f32, + y: f32, + w: f32, + h: f32, text: String, cursor: usize, focused: bool, @@ -135,7 +163,15 @@ struct InputBox { impl InputBox { fn new(x: f32, y: f32, w: f32, h: f32) -> Self { - Self { x, y, w, h, text: String::new(), cursor: 0, focused: false } + Self { + x, + y, + w, + h, + text: String::new(), + cursor: 0, + focused: false, + } } fn hit(&self, px: f32, py: f32) -> bool { px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h @@ -145,20 +181,43 @@ impl InputBox { self.x + 6.0 + tr.measure(&before) } fn insert(&mut self, ch: char) { - let pos = self.text.char_indices().nth(self.cursor).map(|(i,_)|i).unwrap_or(self.text.len()); + let pos = self + .text + .char_indices() + .nth(self.cursor) + .map(|(i, _)| i) + .unwrap_or(self.text.len()); self.text.insert(pos, ch); self.cursor += 1; } fn backspace(&mut self) { if self.cursor > 0 { - let start = self.text.char_indices().nth(self.cursor - 1).map(|(i,_)|i).unwrap_or(0); - let end = self.text.char_indices().nth(self.cursor).map(|(i,_)|i).unwrap_or(self.text.len()); + let start = self + .text + .char_indices() + .nth(self.cursor - 1) + .map(|(i, _)| i) + .unwrap_or(0); + let end = self + .text + .char_indices() + .nth(self.cursor) + .map(|(i, _)| i) + .unwrap_or(self.text.len()); self.text.replace_range(start..end, ""); self.cursor -= 1; } } - fn left(&mut self) { if self.cursor > 0 { self.cursor -= 1; } } - fn right(&mut self) { if self.cursor < self.text.chars().count() { self.cursor += 1; } } + fn left(&mut self) { + if self.cursor > 0 { + self.cursor -= 1; + } + } + fn right(&mut self) { + if self.cursor < self.text.chars().count() { + self.cursor += 1; + } + } } // ============================================================================ @@ -167,11 +226,18 @@ impl InputBox { #[repr(C)] #[derive(Clone, Copy)] -struct ColorVert { pos: [f32; 2], color: [f32; 4] } +struct ColorVert { + pos: [f32; 2], + color: [f32; 4], +} #[repr(C)] #[derive(Clone, Copy)] -struct TextVert { pos: [f32; 2], uv: [f32; 2], color: [f32; 4] } +struct TextVert { + pos: [f32; 2], + uv: [f32; 2], + color: [f32; 4], +} // ============================================================================ // Stage @@ -186,6 +252,7 @@ struct Stage { text_bind: Bindings, tr: TextRenderer, ctx: Box, + #[allow(dead_code)] dpi: f32, } @@ -195,42 +262,117 @@ impl Stage { let dpi = window::dpi_scale(); // Color pipeline - let cs = ctx.new_shader(ShaderSource::Glsl { vertex: COLOR_VS, fragment: COLOR_FS }, - ShaderMeta { images: vec![], uniforms: UniformBlockLayout { uniforms: vec![] } }).unwrap(); - let color_pl = ctx.new_pipeline(&[BufferLayout::default()], - &[VertexAttribute::new("in_pos", VertexFormat::Float2), - VertexAttribute::new("in_color", VertexFormat::Float4)], cs, - PipelineParams { color_blend: Some(BlendState::new(Equation::Add, - BlendFactor::Value(BlendValue::SourceAlpha), - BlendFactor::OneMinusValue(BlendValue::SourceAlpha))), ..Default::default() }); - let cvb = ctx.new_buffer(BufferType::VertexBuffer, BufferUsage::Stream, BufferSource::empty::(1024)); - let cib = ctx.new_buffer(BufferType::IndexBuffer, BufferUsage::Stream, BufferSource::empty::(2048)); - let color_bind = Bindings { vertex_buffers: vec![cvb], index_buffer: cib, images: vec![] }; + let cs = ctx + .new_shader( + ShaderSource::Glsl { + vertex: COLOR_VS, + fragment: COLOR_FS, + }, + ShaderMeta { + images: vec![], + uniforms: UniformBlockLayout { uniforms: vec![] }, + }, + ) + .unwrap(); + let color_pl = ctx.new_pipeline( + &[BufferLayout::default()], + &[ + VertexAttribute::new("in_pos", VertexFormat::Float2), + VertexAttribute::new("in_color", VertexFormat::Float4), + ], + cs, + PipelineParams { + color_blend: Some(BlendState::new( + Equation::Add, + BlendFactor::Value(BlendValue::SourceAlpha), + BlendFactor::OneMinusValue(BlendValue::SourceAlpha), + )), + ..Default::default() + }, + ); + let cvb = ctx.new_buffer( + BufferType::VertexBuffer, + BufferUsage::Stream, + BufferSource::empty::(1024), + ); + let cib = ctx.new_buffer( + BufferType::IndexBuffer, + BufferUsage::Stream, + BufferSource::empty::(2048), + ); + let color_bind = Bindings { + vertex_buffers: vec![cvb], + index_buffer: cib, + images: vec![], + }; // Text renderer & pipeline let tr = TextRenderer::new(&mut ctx, 22.0); - let ts = ctx.new_shader(ShaderSource::Glsl { vertex: TEXT_VS, fragment: TEXT_FS }, - ShaderMeta { images: vec!["tex".into()], uniforms: UniformBlockLayout { uniforms: vec![] } }).unwrap(); - let text_pl = ctx.new_pipeline(&[BufferLayout::default()], - &[VertexAttribute::new("in_pos", VertexFormat::Float2), - VertexAttribute::new("in_uv", VertexFormat::Float2), - VertexAttribute::new("in_color", VertexFormat::Float4)], ts, - PipelineParams { color_blend: Some(BlendState::new(Equation::Add, - BlendFactor::Value(BlendValue::SourceAlpha), - BlendFactor::OneMinusValue(BlendValue::SourceAlpha))), ..Default::default() }); - let tvb = ctx.new_buffer(BufferType::VertexBuffer, BufferUsage::Stream, BufferSource::empty::(4096)); - let tib = ctx.new_buffer(BufferType::IndexBuffer, BufferUsage::Stream, BufferSource::empty::(8192)); - let text_bind = Bindings { vertex_buffers: vec![tvb], index_buffer: tib, images: vec![tr.atlas] }; + let ts = ctx + .new_shader( + ShaderSource::Glsl { + vertex: TEXT_VS, + fragment: TEXT_FS, + }, + ShaderMeta { + images: vec!["tex".into()], + uniforms: UniformBlockLayout { uniforms: vec![] }, + }, + ) + .unwrap(); + let text_pl = ctx.new_pipeline( + &[BufferLayout::default()], + &[ + VertexAttribute::new("in_pos", VertexFormat::Float2), + VertexAttribute::new("in_uv", VertexFormat::Float2), + VertexAttribute::new("in_color", VertexFormat::Float4), + ], + ts, + PipelineParams { + color_blend: Some(BlendState::new( + Equation::Add, + BlendFactor::Value(BlendValue::SourceAlpha), + BlendFactor::OneMinusValue(BlendValue::SourceAlpha), + )), + ..Default::default() + }, + ); + let tvb = ctx.new_buffer( + BufferType::VertexBuffer, + BufferUsage::Stream, + BufferSource::empty::(4096), + ); + let tib = ctx.new_buffer( + BufferType::IndexBuffer, + BufferUsage::Stream, + BufferSource::empty::(8192), + ); + let text_bind = Bindings { + vertex_buffers: vec![tvb], + index_buffer: tib, + images: vec![tr.atlas], + }; let boxes = vec![ InputBox::new(50.0, 80.0, 500.0, 36.0), InputBox::new(50.0, 160.0, 500.0, 36.0), ]; - Self { boxes, focus: None, color_pl, color_bind, text_pl, text_bind, tr, ctx, dpi } + Self { + boxes, + focus: None, + color_pl, + color_bind, + text_pl, + text_bind, + tr, + ctx, + dpi, + } } fn update_ime(&mut self) { + #[cfg(target_os = "windows")] if let Some(i) = self.focus { let b = &self.boxes[i]; let x = (b.cursor_x(&mut self.tr) * self.dpi) as i32; @@ -256,13 +398,31 @@ impl EventHandler for Stage { // Draw boxes for (i, b) in self.boxes.iter().enumerate() { let f = self.focus == Some(i); - let bg = if f { [0.2, 0.2, 0.26, 1.0] } else { [0.16, 0.16, 0.2, 1.0] }; - let br = if f { [0.3, 0.5, 1.0, 1.0] } else { [0.3, 0.3, 0.35, 1.0] }; + let bg = if f { + [0.2, 0.2, 0.26, 1.0] + } else { + [0.16, 0.16, 0.2, 1.0] + }; + let br = if f { + [0.3, 0.5, 1.0, 1.0] + } else { + [0.3, 0.3, 0.35, 1.0] + }; rect(&mut cv, &mut ci, b.x, b.y, b.w, b.h, bg, sw, sh); outline(&mut cv, &mut ci, b.x, b.y, b.w, b.h, br, 2.0, sw, sh); if f { let cx = b.cursor_x(&mut self.tr); - rect(&mut cv, &mut ci, cx, b.y + 6.0, 2.0, b.h - 12.0, [1.0,1.0,1.0,0.9], sw, sh); + rect( + &mut cv, + &mut ci, + cx, + b.y + 6.0, + 2.0, + b.h - 12.0, + [1.0, 1.0, 1.0, 0.9], + sw, + sh, + ); } // Draw text @@ -274,7 +434,7 @@ impl EventHandler for Stage { if g.size[0] > 0.0 { let gx = x + g.offset[0]; let gy = baseline - g.offset[1] - g.size[1]; - glyph_quad(&mut tv, &mut ti, gx, gy, g, [1.0,1.0,1.0,1.0], sw, sh); + glyph_quad(&mut tv, &mut ti, gx, gy, g, [1.0, 1.0, 1.0, 1.0], sw, sh); } x += g.advance; } @@ -289,8 +449,16 @@ impl EventHandler for Stage { self.tr.cache_char(ch); if let Some(g) = self.tr.cache.get(&ch) { if g.size[0] > 0.0 { - glyph_quad(&mut tv, &mut ti, x + g.offset[0], b.y - 24.0 + 16.0 - g.offset[1] - g.size[1], - g, [0.6,0.6,0.65,1.0], sw, sh); + glyph_quad( + &mut tv, + &mut ti, + x + g.offset[0], + b.y - 24.0 + 16.0 - g.offset[1] - g.size[1], + g, + [0.6, 0.6, 0.65, 1.0], + sw, + sh, + ); } x += g.advance; } @@ -298,18 +466,26 @@ impl EventHandler for Stage { } self.tr.flush(&mut self.ctx); - self.ctx.buffer_update(self.color_bind.vertex_buffers[0], BufferSource::slice(&cv)); - self.ctx.buffer_update(self.color_bind.index_buffer, BufferSource::slice(&ci)); - self.ctx.buffer_update(self.text_bind.vertex_buffers[0], BufferSource::slice(&tv)); - self.ctx.buffer_update(self.text_bind.index_buffer, BufferSource::slice(&ti)); + self.ctx + .buffer_update(self.color_bind.vertex_buffers[0], BufferSource::slice(&cv)); + self.ctx + .buffer_update(self.color_bind.index_buffer, BufferSource::slice(&ci)); + self.ctx + .buffer_update(self.text_bind.vertex_buffers[0], BufferSource::slice(&tv)); + self.ctx + .buffer_update(self.text_bind.index_buffer, BufferSource::slice(&ti)); self.ctx.begin_default_pass(Default::default()); self.ctx.apply_pipeline(&self.color_pl); self.ctx.apply_bindings(&self.color_bind); - if !ci.is_empty() { self.ctx.draw(0, ci.len() as i32, 1); } + if !ci.is_empty() { + self.ctx.draw(0, ci.len() as i32, 1); + } self.ctx.apply_pipeline(&self.text_pl); self.ctx.apply_bindings(&self.text_bind); - if !ti.is_empty() { self.ctx.draw(0, ti.len() as i32, 1); } + if !ti.is_empty() { + self.ctx.draw(0, ti.len() as i32, 1); + } self.ctx.end_render_pass(); self.ctx.commit_frame(); } @@ -318,15 +494,17 @@ impl EventHandler for Stage { self.focus = None; for (i, b) in self.boxes.iter_mut().enumerate() { b.focused = b.hit(x, y); - if b.focused { - self.focus = Some(i); + if b.focused { + self.focus = Some(i); // Enable IME when an input box is focused + #[cfg(target_os = "windows")] window::set_ime_enabled(true); } } self.update_ime(); - if self.focus.is_none() { + if self.focus.is_none() { // Disable IME when no input box is focused (for game controls) + #[cfg(target_os = "windows")] window::set_ime_enabled(false); } } @@ -334,18 +512,32 @@ impl EventHandler for Stage { fn key_down_event(&mut self, k: KeyCode, _: KeyMods, _: bool) { if let Some(i) = self.focus { match k { - KeyCode::Backspace => { self.boxes[i].backspace(); self.update_ime(); } - KeyCode::Left => { self.boxes[i].left(); self.update_ime(); } - KeyCode::Right => { self.boxes[i].right(); self.update_ime(); } + KeyCode::Backspace => { + self.boxes[i].backspace(); + self.update_ime(); + } + KeyCode::Left => { + self.boxes[i].left(); + self.update_ime(); + } + KeyCode::Right => { + self.boxes[i].right(); + self.update_ime(); + } KeyCode::Enter => { - if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("ime_output.txt") { + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("ime_output.txt") + { let _ = writeln!(f, "Input {}: \"{}\"", i + 1, self.boxes[i].text); } } - KeyCode::Escape => { - self.boxes[i].focused = false; - self.focus = None; + KeyCode::Escape => { + self.boxes[i].focused = false; + self.focus = None; // Disable IME when focus is lost + #[cfg(target_os = "windows")] window::set_ime_enabled(false); } _ => {} @@ -354,7 +546,9 @@ impl EventHandler for Stage { } fn char_event(&mut self, ch: char, _: KeyMods, _: bool) { - if ch.is_control() { return; } + if ch.is_control() { + return; + } if let Some(i) = self.focus { self.boxes[i].insert(ch); self.update_ime(); @@ -366,28 +560,99 @@ impl EventHandler for Stage { // Helpers // ============================================================================ -fn rect(v: &mut Vec, i: &mut Vec, x: f32, y: f32, w: f32, h: f32, c: [f32;4], sw: f32, sh: f32) { +fn rect( + v: &mut Vec, + i: &mut Vec, + x: f32, + y: f32, + w: f32, + h: f32, + c: [f32; 4], + sw: f32, + sh: f32, +) { let b = v.len() as u16; - let (x0, y0) = ((x/sw)*2.0-1.0, 1.0-(y/sh)*2.0); - let (x1, y1) = (((x+w)/sw)*2.0-1.0, 1.0-((y+h)/sh)*2.0); - v.extend([ColorVert{pos:[x0,y0],color:c}, ColorVert{pos:[x1,y0],color:c}, - ColorVert{pos:[x1,y1],color:c}, ColorVert{pos:[x0,y1],color:c}]); - i.extend([b, b+1, b+2, b, b+2, b+3]); + let (x0, y0) = ((x / sw) * 2.0 - 1.0, 1.0 - (y / sh) * 2.0); + let (x1, y1) = (((x + w) / sw) * 2.0 - 1.0, 1.0 - ((y + h) / sh) * 2.0); + v.extend([ + ColorVert { + pos: [x0, y0], + color: c, + }, + ColorVert { + pos: [x1, y0], + color: c, + }, + ColorVert { + pos: [x1, y1], + color: c, + }, + ColorVert { + pos: [x0, y1], + color: c, + }, + ]); + i.extend([b, b + 1, b + 2, b, b + 2, b + 3]); } -fn outline(v: &mut Vec, i: &mut Vec, x: f32, y: f32, w: f32, h: f32, c: [f32;4], t: f32, sw: f32, sh: f32) { - rect(v,i,x,y,w,t,c,sw,sh); rect(v,i,x,y+h-t,w,t,c,sw,sh); - rect(v,i,x,y,t,h,c,sw,sh); rect(v,i,x+w-t,y,t,h,c,sw,sh); +fn outline( + v: &mut Vec, + i: &mut Vec, + x: f32, + y: f32, + w: f32, + h: f32, + c: [f32; 4], + t: f32, + sw: f32, + sh: f32, +) { + rect(v, i, x, y, w, t, c, sw, sh); + rect(v, i, x, y + h - t, w, t, c, sw, sh); + rect(v, i, x, y, t, h, c, sw, sh); + rect(v, i, x + w - t, y, t, h, c, sw, sh); } -fn glyph_quad(v: &mut Vec, i: &mut Vec, x: f32, y: f32, g: &GlyphInfo, c: [f32;4], sw: f32, sh: f32) { +fn glyph_quad( + v: &mut Vec, + i: &mut Vec, + x: f32, + y: f32, + g: &GlyphInfo, + c: [f32; 4], + sw: f32, + sh: f32, +) { let b = v.len() as u16; - let (x0, y0) = ((x/sw)*2.0-1.0, 1.0-(y/sh)*2.0); - let (x1, y1) = (((x+g.size[0])/sw)*2.0-1.0, 1.0-((y+g.size[1])/sh)*2.0); - let (u0, v0, u1, v1) = (g.uv[0], g.uv[1], g.uv[0]+g.uv[2], g.uv[1]+g.uv[3]); - v.extend([TextVert{pos:[x0,y0],uv:[u0,v0],color:c}, TextVert{pos:[x1,y0],uv:[u1,v0],color:c}, - TextVert{pos:[x1,y1],uv:[u1,v1],color:c}, TextVert{pos:[x0,y1],uv:[u0,v1],color:c}]); - i.extend([b, b+1, b+2, b, b+2, b+3]); + let (x0, y0) = ((x / sw) * 2.0 - 1.0, 1.0 - (y / sh) * 2.0); + let (x1, y1) = ( + ((x + g.size[0]) / sw) * 2.0 - 1.0, + 1.0 - ((y + g.size[1]) / sh) * 2.0, + ); + let (u0, v0, u1, v1) = (g.uv[0], g.uv[1], g.uv[0] + g.uv[2], g.uv[1] + g.uv[3]); + v.extend([ + TextVert { + pos: [x0, y0], + uv: [u0, v0], + color: c, + }, + TextVert { + pos: [x1, y0], + uv: [u1, v0], + color: c, + }, + TextVert { + pos: [x1, y1], + uv: [u1, v1], + color: c, + }, + TextVert { + pos: [x0, y1], + uv: [u0, v1], + color: c, + }, + ]); + i.extend([b, b + 1, b + 2, b, b + 2, b + 3]); } // ============================================================================ @@ -395,7 +660,8 @@ fn glyph_quad(v: &mut Vec, i: &mut Vec, x: f32, y: f32, g: &Glyph // ============================================================================ const COLOR_VS: &str = "#version 100\nattribute vec2 in_pos; attribute vec4 in_color; varying lowp vec4 color;\nvoid main() { gl_Position = vec4(in_pos, 0.0, 1.0); color = in_color; }"; -const COLOR_FS: &str = "#version 100\nvarying lowp vec4 color; void main() { gl_FragColor = color; }"; +const COLOR_FS: &str = + "#version 100\nvarying lowp vec4 color; void main() { gl_FragColor = color; }"; const TEXT_VS: &str = "#version 100\nattribute vec2 in_pos; attribute vec2 in_uv; attribute vec4 in_color;\nvarying lowp vec2 uv; varying lowp vec4 color;\nvoid main() { gl_Position = vec4(in_pos, 0.0, 1.0); uv = in_uv; color = in_color; }"; const TEXT_FS: &str = "#version 100\nprecision mediump float; varying lowp vec2 uv; varying lowp vec4 color; uniform sampler2D tex;\nvoid main() { gl_FragColor = vec4(color.rgb, color.a * texture2D(tex, uv).a); }"; @@ -404,10 +670,13 @@ const TEXT_FS: &str = "#version 100\nprecision mediump float; varying lowp vec2 // ============================================================================ fn main() { - miniquad::start(conf::Conf { - window_title: "IME Test - Chinese Input".into(), - window_width: 640, - window_height: 300, - ..Default::default() - }, || Box::new(Stage::new())); + miniquad::start( + conf::Conf { + window_title: "IME Test - Chinese Input".into(), + window_width: 640, + window_height: 300, + ..Default::default() + }, + || Box::new(Stage::new()), + ); } diff --git a/examples/mouse_cursor.rs b/examples/mouse_cursor.rs index 63320c8ea..800ef9914 100644 --- a/examples/mouse_cursor.rs +++ b/examples/mouse_cursor.rs @@ -7,14 +7,15 @@ impl EventHandler for Stage { fn draw(&mut self) {} - fn char_event(&mut self, character: char, _: KeyMods, _: bool) { - match character { + fn char_event(&mut self, _character: char, _: KeyMods, _: bool) { + #[cfg(target_os = "windows")] + match _character { 'z' => window::show_mouse(false), 'x' => window::show_mouse(true), _ => (), } - let icon = match character { + let _icon = match _character { '1' => CursorIcon::Default, '2' => CursorIcon::Help, '3' => CursorIcon::Pointer, @@ -29,7 +30,8 @@ impl EventHandler for Stage { 'w' => CursorIcon::NWSEResize, _ => return, }; - window::set_mouse_cursor(icon); + #[cfg(target_os = "windows")] + window::set_mouse_cursor(_icon); } } diff --git a/src/event.rs b/src/event.rs index 82e0a9258..a30f6767c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -177,7 +177,7 @@ pub trait EventHandler { fn mouse_button_down_event(&mut self, _button: MouseButton, _x: f32, _y: f32) {} fn mouse_button_up_event(&mut self, _button: MouseButton, _x: f32, _y: f32) {} fn mouse_leave_event(&mut self) {} - fn mouse_enter_event(&mut self, _button: MouseButton ,_x: f32, _y: f32 ) {} + fn mouse_enter_event(&mut self, _button: MouseButton, _x: f32, _y: f32) {} fn char_event(&mut self, _character: char, _keymods: KeyMods, _repeat: bool) {} diff --git a/src/native/linux_x11.rs b/src/native/linux_x11.rs index 0fea8e266..811eb31dd 100644 --- a/src/native/linux_x11.rs +++ b/src/native/linux_x11.rs @@ -137,8 +137,7 @@ impl X11Display { _ if (state & Button3MotionMask) != 0 => MouseButton::Right, _ => MouseButton::Unknown, }; - event_handler.mouse_enter_event( btn , x, y); - + event_handler.mouse_enter_event(btn, x, y); } 8 => { event_handler.mouse_leave_event(); diff --git a/src/native/windows.rs b/src/native/windows.rs index 01ffe5fa4..018cf3547 100644 --- a/src/native/windows.rs +++ b/src/native/windows.rs @@ -16,7 +16,7 @@ use winapi::{ windowsx::{GET_X_LPARAM, GET_Y_LPARAM}, }, um::{ - imm::{HIMC, ImmGetContext, ImmReleaseContext}, + imm::{ImmGetContext, ImmReleaseContext, HIMC}, libloaderapi::{GetModuleHandleW, GetProcAddress}, shellapi::{DragAcceptFiles, DragQueryFileW, HDROP}, shellscalingapi::*, @@ -76,7 +76,12 @@ struct CANDIDATEFORM { // Link to imm32.dll for IME support #[link(name = "imm32")] extern "system" { - fn ImmGetCompositionStringW(himc: HIMC, index: DWORD, buf: *mut std::ffi::c_void, len: DWORD) -> i32; + fn ImmGetCompositionStringW( + himc: HIMC, + index: DWORD, + buf: *mut std::ffi::c_void, + len: DWORD, + ) -> i32; fn ImmAssociateContextEx(hwnd: HWND, himc: HIMC, flags: DWORD) -> i32; fn ImmAssociateContext(hwnd: HWND, himc: HIMC) -> HIMC; fn ImmCreateContext() -> HIMC; @@ -165,7 +170,7 @@ impl WindowsDisplay { ImmReleaseContext(self.wnd, himc); } } - + /// Enable or disable IME for the window. /// When disabled, the IME will not process keyboard input, useful for game controls. fn set_ime_enabled(&mut self, enabled: bool) { @@ -181,7 +186,7 @@ impl WindowsDisplay { } } } - + fn set_mouse_cursor(&mut self, cursor_icon: CursorIcon) { let cursor_name = match cursor_icon { CursorIcon::Default => IDC_ARROW, @@ -590,19 +595,20 @@ unsafe extern "system" fn win32_wndproc( } WM_IME_COMPOSITION => { let flags = lparam as u32; - + // Extract and dispatch the result string manually to avoid duplicates if (flags & GCS_RESULTSTR) != 0 { let himc = ImmGetContext(hwnd); if !himc.is_null() { - let len = ImmGetCompositionStringW(himc, GCS_RESULTSTR, std::ptr::null_mut(), 0); + let len = + ImmGetCompositionStringW(himc, GCS_RESULTSTR, std::ptr::null_mut(), 0); if len > 0 { let mut buffer: Vec = vec![0; (len as usize / 2) + 1]; let actual_len = ImmGetCompositionStringW( - himc, - GCS_RESULTSTR, - buffer.as_mut_ptr() as *mut _, - len as u32 + himc, + GCS_RESULTSTR, + buffer.as_mut_ptr() as *mut _, + len as u32, ); if actual_len > 0 { let char_count = actual_len as usize / 2; @@ -620,47 +626,60 @@ unsafe extern "system" fn win32_wndproc( } return 0; } - + // For non-result messages (composition state updates), pass to DefWindowProc return DefWindowProcW(hwnd, umsg, wparam, lparam); } WM_IME_SETCONTEXT => { let user_disabled = IME_USER_DISABLED.load(std::sync::atomic::Ordering::Relaxed); - + // If user explicitly disabled IME, don't auto-restore if user_disabled { return 0; } - + // Must pass to DefWindowProc to enable IME properly return DefWindowProcW(hwnd, umsg, wparam, lparam); } WM_IME_STARTCOMPOSITION => { // Offset for candidate window below composition position const CANDIDATE_WINDOW_Y_OFFSET: i32 = 20; - + // Set candidate window position when IME starts composition let himc = ImmGetContext(hwnd); if !himc.is_null() { let mut pt: POINT = std::mem::zeroed(); GetCaretPos(&mut pt); - + let comp_form = COMPOSITIONFORM { dwStyle: CFS_POINT, ptCurrentPos: pt, - rcArea: RECT { left: 0, top: 0, right: 0, bottom: 0 }, + rcArea: RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, }; ImmSetCompositionWindow(himc, &comp_form); - + // Set candidate window position (most IMEs only use index 0) let cand_form = CANDIDATEFORM { dwIndex: 0, dwStyle: CFS_CANDIDATEPOS, - ptCurrentPos: POINT { x: pt.x, y: pt.y + CANDIDATE_WINDOW_Y_OFFSET }, - rcArea: RECT { left: 0, top: 0, right: 0, bottom: 0 }, + ptCurrentPos: POINT { + x: pt.x, + y: pt.y + CANDIDATE_WINDOW_Y_OFFSET, + }, + rcArea: RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, }; ImmSetCandidateWindow(himc, &cand_form); - + ImmReleaseContext(hwnd, himc); } return DefWindowProcW(hwnd, umsg, wparam, lparam); @@ -670,20 +689,21 @@ unsafe extern "system" fn win32_wndproc( } WM_IME_NOTIFY => { const IMN_SETOPENSTATUS: WPARAM = 0x0008; - + // Re-enable IME if it was unexpectedly closed (unless user disabled it) if wparam == IMN_SETOPENSTATUS { let himc = ImmGetContext(hwnd); if !himc.is_null() { let open_status = ImmGetOpenStatus(himc); - let user_disabled = IME_USER_DISABLED.load(std::sync::atomic::Ordering::Relaxed); + let user_disabled = + IME_USER_DISABLED.load(std::sync::atomic::Ordering::Relaxed); if open_status == 0 && !user_disabled { ImmSetOpenStatus(himc, 1); } ImmReleaseContext(hwnd, himc); } } - + return DefWindowProcW(hwnd, umsg, wparam, lparam); } WM_INPUTLANGCHANGEREQUEST | WM_INPUTLANGCHANGE => { @@ -769,11 +789,11 @@ unsafe extern "system" fn win32_wndproc( } WM_SETFOCUS => { let user_disabled = IME_USER_DISABLED.load(std::sync::atomic::Ordering::Relaxed); - + // Ensure IME context is available when window gains focus if !user_disabled { let himc = ImmGetContext(hwnd); - + if himc.is_null() { // Create new IME context if none exists let new_himc = ImmCreateContext(); @@ -793,7 +813,7 @@ unsafe extern "system" fn win32_wndproc( ImmReleaseContext(hwnd, himc); } } - + return DefWindowProcW(hwnd, umsg, wparam, lparam); } WM_KILLFOCUS => { @@ -978,14 +998,14 @@ unsafe fn create_window( unsafe fn create_msg_window() -> (HWND, HDC) { // Use a separate window class to avoid interfering with main window's IME let class_name = "MINIQUADMSGWND\0".encode_utf16().collect::>(); - + let mut wndclassw: WNDCLASSW = std::mem::zeroed(); wndclassw.style = 0; wndclassw.lpfnWndProc = Some(DefWindowProcW); wndclassw.hInstance = GetModuleHandleW(NULL as _); wndclassw.lpszClassName = class_name.as_ptr() as _; RegisterClassW(&wndclassw); - + let window_name = "miniquad message window\0" .encode_utf16() .collect::>(); @@ -1007,10 +1027,10 @@ unsafe fn create_msg_window() -> (HWND, HDC) { !msg_hwnd.is_null(), "Win32: failed to create helper window!" ); - + // Disable IME for message window to avoid interfering with main window ImmAssociateContextEx(msg_hwnd, std::ptr::null_mut(), IACE_CHILDREN); - + ShowWindow(msg_hwnd, SW_HIDE); let mut msg = std::mem::zeroed(); while PeekMessageW(&mut msg as _, msg_hwnd, 0, 0, PM_REMOVE) != 0 { From a8ae56fbf899d056db52408290cc1fecf22f2dc4 Mon Sep 17 00:00:00 2001 From: ljlvink Date: Sun, 22 Mar 2026 23:51:56 +0800 Subject: [PATCH 2/3] feat: add initial support for OpenHarmony --- Cargo.toml | 22 ++ README.md | 76 +++++- build.rs | 2 + src/fs.rs | 14 +- src/lib.rs | 57 ++++- src/native.rs | 24 +- src/native/egl.rs | 12 +- src/native/ohos.rs | 486 ++++++++++++++++++++++++++++++++++++ src/native/ohos/keycodes.rs | 107 ++++++++ 9 files changed, 779 insertions(+), 21 deletions(-) create mode 100644 src/native/ohos.rs create mode 100644 src/native/ohos/keycodes.rs diff --git a/Cargo.toml b/Cargo.toml index 224bebe17..8119b65ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,28 @@ log-impl = [] [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2" +[target.'cfg(target_env = "ohos")'.dependencies] +ohos-xcomponent-binding = "0.1" +raw-window-handle = "0.6" +napi-ohos = { version = "1.1.3", default-features = false, features = [ + "napi8", + "async", +] } +napi-derive-ohos = { version = "1.1.3" } +ohos-hilog-binding = { version = "0.1.2", features = [ + "redirect" +] } +xcomponent-sys = { version = "0.3.4", features = ["api-14", "keyboard-types"] } +keyboard-types = "0.8.3" +ohos-xcomponent-sys = { version = "0.0.2" } +ohos_enum_macro = { version = "0.0.2" } +ohos-input-sys = { version = "0.3.2", features = ["api-17"] } +ohos-qos-sys = { version = "0.0.1", features = ["api-20"] } +ohos-native-window-sys = { version = "0.0.1" } + +[target.'cfg(target_env = "ohos")'.build-dependencies] +napi-build-ohos = { version = "1.1.3" } + [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = [ "wingdi", diff --git a/README.md b/README.md index 7486c44f6..fc65fb374 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Miniquad aims to provide a graphics abstraction that works the same way on any p * macOS, OpenGL 3, Metal; * iOS, GLES 2, GLES 3, Metal; * WASM, WebGL 1 - tested on iOS Safari, Firefox, Chrome; -* Android, GLES 2, GLES 3. +* Android, GLES 2, GLES 3; +* OpenHarmony, GLES 2, GLES 3. ## Examples @@ -150,6 +151,79 @@ xcrun simctl launch booted com.mygame For details and instructions on provisioning for real iphone, check [https://macroquad.rs/articles/ios/](https://macroquad.rs/articles/ios/) +## OpenHarmony (OHOS) + +Due to limitations in ohrs, running examples requires extracting the example project separately and marking the C entry point: + +Ensure your example crate type is set to `cdylib` + +```rust +#[no_mangle] +pub extern "C" fn quad_main() +``` + +Assume your project is named `nativerender`. + +### Prerequisites + +```bash +rustup target add aarch64-unknown-linux-ohos +rustup target add armv7-unknown-linux-ohos +rustup target add x86_64-unknown-linux-ohos +cargo install ohrs +``` + +### 1. Create an XComponent in Your App using DevEco Studio + +```typescript +@Entry +@Component +struct Index { + @State message: string = 'Hello World' + xComponentContext: ESObject | undefined = undefined; + xComponentAttrs: XComponentAttrs = { + id: 'xcomponentId', + type: XComponentType.SURFACE, + libraryname: 'nativerender' + } + + build() { + Row() { + Button("draw").onClick(() => { + this.xComponentContext!.drawXcomponent(); + }) + // ... + // Define XComponent + XComponent(this.xComponentAttrs) + .focusable(true) // Enable keyboard events + .onLoad((xComponentContext) => { + this.xComponentContext = xComponentContext; + }) + .onDestroy(() => { + console.log("onDestroy"); + }) + // ... + } + .height('100%') + } +} + +interface XComponentAttrs { + id: string; + type: number; + libraryname: string; +} +``` + +### 2. Build with ohrs + +```bash +ohrs build --arch aarch +``` + +Copy the `libnativerender.so` file from `dist/arm64-v8a` or `dist/x86_64` to your Harmony project. + + ## Cross Compilation ```bash diff --git a/build.rs b/build.rs index 50ba1b615..089f8535d 100644 --- a/build.rs +++ b/build.rs @@ -6,4 +6,6 @@ fn main() { if target.contains("darwin") || target.contains("ios") { println!("cargo:rustc-link-lib=framework=MetalKit"); } + #[cfg(target_env = "ohos")] + napi_build_ohos::setup(); } diff --git a/src/fs.rs b/src/fs.rs index 2212b042a..a27e6f0a8 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,5 +1,7 @@ #[cfg(target_os = "ios")] use crate::native::ios; +#[cfg(target_env = "ohos")] +use crate::native::ohos; #[derive(Debug)] pub enum Error { @@ -45,7 +47,15 @@ pub fn load_file(path: &str, on_loaded: F) { #[cfg(target_os = "ios")] ios::load_file(path, on_loaded); - #[cfg(not(any(target_arch = "wasm32", target_os = "android", target_os = "ios")))] + #[cfg(target_env = "ohos")] + ohos::load_file(path, on_loaded); + + #[cfg(not(any( + target_arch = "wasm32", + target_os = "android", + target_os = "ios", + target_env = "ohos" + )))] load_file_desktop(path, on_loaded); } @@ -122,7 +132,7 @@ mod wasm { } } -#[cfg(not(any(target_arch = "wasm32", target_os = "android", target_os = "ios")))] +#[cfg(not(any(target_arch = "wasm32", target_os = "android", target_os = "ios", target_env = "ohos")))] fn load_file_desktop(path: &str, on_loaded: F) { fn load_file_sync(path: &str) -> Response { use std::fs::File; diff --git a/src/lib.rs b/src/lib.rs index e7f9d5004..5afac1c8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,15 @@ clippy::missing_safety_doc )] +#[cfg(target_env = "ohos")] +use napi_derive_ohos::napi; + +#[cfg(target_env = "ohos")] +use napi_ohos::{bindgen_prelude::Object, Env, Result}; + +#[cfg(target_env = "ohos")] +use ohos_hilog_binding::forward_stdio_to_hilog; + pub mod conf; mod event; pub mod fs; @@ -209,6 +218,7 @@ pub mod window { /// NOTICE: on desktop cursor will not be automatically released after window lost focus /// so set_cursor_grab(false) on window's focus lost is recommended. /// TODO: implement window focus events + #[cfg(not(target_env = "ohos"))] pub fn set_cursor_grab(grab: bool) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -230,13 +240,16 @@ pub mod window { /// /// Does nothing without `conf.platform.blocking_event_loop`. pub fn schedule_update() { - #[cfg(all(target_os = "android", not(target_arch = "wasm32")))] + #[cfg(all( + any(target_os = "android", target_env = "ohos"), + not(target_arch = "wasm32") + ))] { let d = native_display().lock().unwrap(); (d.native_requests)(native::Request::ScheduleUpdate); } - #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] + #[cfg(not(any(target_arch = "wasm32", target_os = "android", target_env = "ohos")))] { let d = native_display().lock().unwrap(); d.native_requests @@ -251,6 +264,7 @@ pub mod window { } /// Show or hide the mouse cursor + #[cfg(not(target_env = "ohos"))] pub fn show_mouse(shown: bool) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -267,6 +281,7 @@ pub mod window { } /// Set the mouse cursor icon. + #[cfg(not(target_env = "ohos"))] pub fn set_mouse_cursor(cursor_icon: CursorIcon) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -283,6 +298,7 @@ pub mod window { } /// Set the application's window size. + #[cfg(not(target_env = "ohos"))] pub fn set_window_size(new_width: u32, new_height: u32) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -303,7 +319,7 @@ pub mod window { .unwrap(); } } - + #[cfg(not(target_env = "ohos"))] pub fn set_window_position(new_x: u32, new_y: u32) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -321,12 +337,16 @@ pub mod window { /// Get the position of the window. /// TODO: implement for other platforms - #[cfg(any(target_os = "windows", target_os = "linux"))] + #[cfg(any( + target_os = "windows", + all(target_os = "linux", not(target_env = "ohos")) + ))] pub fn get_window_position() -> (u32, u32) { let d = native_display().lock().unwrap(); d.screen_position } + #[cfg(not(target_env = "ohos"))] pub fn set_fullscreen(fullscreen: bool) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -368,6 +388,7 @@ pub mod window { /// Show/hide onscreen keyboard. /// Only works on Android right now. + #[cfg(not(target_env = "ohos"))] pub fn show_keyboard(show: bool) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -387,6 +408,7 @@ pub mod window { /// The position is in window client coordinates (pixels). /// This should be called when the text cursor moves to keep the IME /// candidate window near the insertion point. + #[cfg(not(target_env = "ohos"))] pub fn set_ime_position(x: i32, y: i32) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -409,6 +431,7 @@ pub mod window { /// /// # Arguments /// * `enabled` - `true` to enable IME (for text input), `false` to disable (for game controls) + #[cfg(not(target_env = "ohos"))] pub fn set_ime_enabled(enabled: bool) { let d = native_display().lock().unwrap(); #[cfg(target_os = "android")] @@ -462,7 +485,11 @@ pub fn start(conf: conf::Conf, f: F) where F: 'static + FnOnce() -> Box, { - #[cfg(target_os = "linux")] + #[cfg(target_env = "ohos")] + unsafe { + native::ohos::run(conf, f); + } + #[cfg(all(target_os = "linux", not(target_env = "ohos")))] { let mut f = Some(f); let f = &mut f; @@ -514,3 +541,23 @@ where native::ios::run(conf, f); } } + +#[cfg(target_env = "ohos")] +extern "C" { + fn quad_main(); +} + +static mut OHOS_EXPORTS: Option> = None; +static mut OHOS_ENV: Option = None; + +#[cfg(target_env = "ohos")] +#[napi(module_exports)] +pub fn init(exports: Object, env: Env) -> Result<()> { + let _ = forward_stdio_to_hilog(); + unsafe { + OHOS_EXPORTS = Some(std::mem::transmute(exports)); + OHOS_ENV = Some(env); + quad_main(); + } + Ok(()) +} diff --git a/src/native.rs b/src/native.rs index f38200360..48570bddb 100644 --- a/src/native.rs +++ b/src/native.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -#[cfg(not(target_os = "android"))] +#[cfg(not(any(target_os = "android", target_env = "ohos")))] use std::sync::mpsc; #[derive(Default)] @@ -16,9 +16,9 @@ pub(crate) struct NativeDisplayData { pub high_dpi: bool, pub quit_requested: bool, pub quit_ordered: bool, - #[cfg(target_os = "android")] + #[cfg(any(target_os = "android", target_env = "ohos"))] pub native_requests: Box, - #[cfg(not(target_os = "android"))] + #[cfg(not(any(target_os = "android", target_env = "ohos")))] pub native_requests: mpsc::Sender, pub clipboard: Box, pub dropped_files: DroppedFiles, @@ -40,8 +40,12 @@ impl NativeDisplayData { pub fn new( screen_width: i32, screen_height: i32, - #[cfg(target_os = "android")] native_requests: Box, - #[cfg(not(target_os = "android"))] native_requests: mpsc::Sender, + #[cfg(any(target_os = "android", target_env = "ohos"))] native_requests: Box< + dyn Fn(Request) + Send, + >, + #[cfg(not(any(target_os = "android", target_env = "ohos")))] native_requests: mpsc::Sender< + Request, + >, clipboard: Box, ) -> NativeDisplayData { NativeDisplayData { @@ -87,10 +91,10 @@ pub trait Clipboard: Send + Sync { pub mod module; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(target_env = "ohos")))] pub mod linux_x11; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(target_env = "ohos")))] pub mod linux_wayland; #[cfg(target_os = "android")] @@ -114,6 +118,12 @@ pub mod macos; #[cfg(target_os = "ios")] pub mod ios; +#[cfg(target_env = "ohos")] +pub mod ohos; + +#[cfg(target_env = "ohos")] +pub use ohos::*; + #[cfg(any(target_os = "android", target_os = "linux"))] pub mod egl; diff --git a/src/native/egl.rs b/src/native/egl.rs index 5dc24e27f..c45ed7078 100644 --- a/src/native/egl.rs +++ b/src/native/egl.rs @@ -1,17 +1,17 @@ #![allow(non_camel_case_types, non_snake_case, dead_code)] -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(target_env = "ohos")))] pub type EGLNativeDisplayType = *mut crate::native::linux_x11::libx11::Display; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(target_env = "ohos")))] pub type EGLNativePixmapType = crate::native::linux_x11::libx11::Pixmap; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(target_env = "ohos")))] pub type EGLNativeWindowType = crate::native::linux_x11::libx11::Window; -#[cfg(target_os = "android")] +#[cfg(any(target_os = "android", target_env = "ohos"))] pub type EGLNativeDisplayType = *mut (); -#[cfg(target_os = "android")] +#[cfg(any(target_os = "android", target_env = "ohos"))] pub type EGLNativePixmapType = ::core::ffi::c_ulong; -#[cfg(target_os = "android")] +#[cfg(any(target_os = "android", target_env = "ohos"))] pub type EGLNativeWindowType = ::core::ffi::c_ulong; pub use core::ptr::null_mut; diff --git a/src/native/ohos.rs b/src/native/ohos.rs new file mode 100644 index 000000000..c681120ad --- /dev/null +++ b/src/native/ohos.rs @@ -0,0 +1,486 @@ +use crate::{ + event::{EventHandler, KeyCode, KeyMods, TouchPhase}, + native::{ + egl::{self, LibEgl}, + NativeDisplayData, + }, +}; +use ohos_hilog_binding::{hilog_error, hilog_fatal, hilog_info}; +use ohos_qos_sys::{OH_QoS_SetThreadQoS, QoS_Level_QOS_USER_INTERACTIVE}; +use ohos_xcomponent_binding::{WindowRaw, XComponent}; +use ohos_xcomponent_sys::{ + OH_NativeXComponent, OH_NativeXComponent_ExpectedRateRange, OH_NativeXComponent_GetKeyEvent, + OH_NativeXComponent_GetKeyEventAction, OH_NativeXComponent_GetKeyEventCode, + OH_NativeXComponent_RegisterKeyEventCallback, OH_NativeXComponent_SetExpectedFrameRateRange, +}; +mod keycodes; + +use crate::{OHOS_ENV, OHOS_EXPORTS}; +use std::{cell::RefCell, sync::mpsc, thread}; + +#[derive(Debug)] +enum Message { + SurfaceChanged { + width: i32, + height: i32, + }, + SurfaceCreated { + window: WindowRaw, + }, + SurfaceDestroyed, + Touch { + phase: TouchPhase, + touch_id: u64, + x: f32, + y: f32, + }, + Character { + character: u32, + }, + KeyDown { + keycode: KeyCode, + }, + KeyUp { + keycode: KeyCode, + }, + Pause, + Resume, + Destroy, + Request(crate::native::Request), +} + +unsafe impl Send for Message {} + +thread_local! { + static MESSAGES_TX: RefCell>> = RefCell::new(None); +} + +fn send_message(message: Message) { + MESSAGES_TX.with(|tx| { + let mut tx = tx.borrow_mut(); + tx.as_mut().unwrap().send(message).unwrap(); + }) +} + +struct MainThreadState { + libegl: LibEgl, + egl_display: egl::EGLDisplay, + egl_config: egl::EGLConfig, + egl_context: egl::EGLContext, + surface: egl::EGLSurface, + window: WindowRaw, + event_handler: Box, + quit: bool, + fullscreen: bool, + update_requested: bool, + keymods: KeyMods, +} + +impl MainThreadState { + unsafe fn destroy_surface(&mut self) { + (self.libegl.eglMakeCurrent)( + self.egl_display, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + (self.libegl.eglDestroySurface)(self.egl_display, self.surface); + self.surface = std::ptr::null_mut(); + } + + unsafe fn update_surface(&mut self, window: WindowRaw) { + self.window = window; + if self.surface.is_null() == false { + self.destroy_surface(); + } + self.surface = (self.libegl.eglCreateWindowSurface)( + self.egl_display, + self.egl_config, + window.0 as _, + std::ptr::null_mut(), + ); + + if self.surface.is_null() { + let error = (self.libegl.eglGetError)(); + hilog_fatal!(format!( + "Failed to create EGL window surface, EGL error: {}", + error + )); + return; + } + let res = (self.libegl.eglMakeCurrent)( + self.egl_display, + self.surface, + self.surface, + self.egl_context, + ); + + if res == 0 { + let error = (self.libegl.eglGetError)(); + hilog_fatal!(format!( + "Failed to make EGL context current, EGL error: {}", + error + )); + } + } + + fn process_message(&mut self, msg: Message) { + match msg { + Message::SurfaceCreated { window } => unsafe { + self.update_surface(window); + }, + Message::SurfaceDestroyed => unsafe { + self.destroy_surface(); + }, + Message::SurfaceChanged { width, height } => { + { + let mut d = crate::native_display().lock().unwrap(); + d.screen_width = width as _; + d.screen_height = height as _; + } + self.event_handler.resize_event(width as _, height as _); + if self.surface.is_null() { + hilog_info!("Received SurfaceChanged but no surface exists yet"); + } + } + Message::Touch { + phase, + touch_id, + x, + y, + } => { + self.event_handler.touch_event(phase, touch_id, x, y); + } + Message::Character { character } => { + if let Some(character) = char::from_u32(character) { + self.event_handler + .char_event(character, Default::default(), false); + } + } + Message::KeyDown { keycode } => { + match keycode { + KeyCode::LeftShift | KeyCode::RightShift => self.keymods.shift = true, + KeyCode::LeftControl | KeyCode::RightControl => self.keymods.ctrl = true, + KeyCode::LeftAlt | KeyCode::RightAlt => self.keymods.alt = true, + KeyCode::LeftSuper | KeyCode::RightSuper => self.keymods.logo = true, + _ => {} + } + self.event_handler + .key_down_event(keycode, self.keymods, false); + } + Message::KeyUp { keycode } => { + match keycode { + KeyCode::LeftShift | KeyCode::RightShift => self.keymods.shift = false, + KeyCode::LeftControl | KeyCode::RightControl => self.keymods.ctrl = false, + KeyCode::LeftAlt | KeyCode::RightAlt => self.keymods.alt = false, + KeyCode::LeftSuper | KeyCode::RightSuper => self.keymods.logo = false, + _ => {} + } + self.event_handler.key_up_event(keycode, self.keymods); + } + Message::Pause => self.event_handler.window_minimized_event(), + Message::Resume => self.event_handler.window_restored_event(), + Message::Destroy => { + self.quit = true; + self.event_handler.quit_requested_event() + } + Message::Request(req) => self.process_request(req), + } + } + + fn frame(&mut self) { + self.event_handler.update(); + if !self.surface.is_null() { + self.update_requested = false; + self.event_handler.draw(); + unsafe { + (self.libegl.eglSwapBuffers)(self.egl_display, self.surface); + } + } + } + + fn process_request(&mut self, request: crate::native::Request) { + use crate::native::Request::*; + + match request { + ScheduleUpdate => { + self.update_requested = true; + } + SetFullscreen(_) => {} //not support currently + ShowKeyboard(_) => {} //not support currently + _ => {} + } + } +} +fn register_xcomponent_callbacks(xcomponent: &XComponent) -> napi_ohos::Result<()> { + let native_xcomponent = xcomponent.raw(); + let res = unsafe { + OH_NativeXComponent_RegisterKeyEventCallback(native_xcomponent, Some(on_dispatch_key_event)) + }; + if res != 0 { + hilog_error!("Failed to register key event callbacks"); + } else { + hilog_info!("Registered key event callbacks successfully"); + } + + Ok(()) +} + +fn set_display_sync(xcomponent: &XComponent) -> bool { + let native_xcomponent = xcomponent.raw(); + let mut expected_rate_range = OH_NativeXComponent_ExpectedRateRange { + min: 110, + max: 120, + expected: 120, + }; + let res = unsafe { + OH_NativeXComponent_SetExpectedFrameRateRange(native_xcomponent, &mut expected_rate_range) + }; + hilog_info!("Set display sync: {}", res); + res == 0 +} +pub unsafe extern "C" fn on_dispatch_key_event( + xcomponent: *mut OH_NativeXComponent, + _: *mut std::os::raw::c_void, +) { + let mut event = std::ptr::null_mut(); + let ret = OH_NativeXComponent_GetKeyEvent(xcomponent, &mut event); + assert!(ret == 0, "Get key event failed"); + + let mut action = 0; + let ret = OH_NativeXComponent_GetKeyEventAction(event, &mut action); + assert!(ret == 0, "Get key event action failed"); + + let code = ohos_input_sys::key_code::Input_KeyCode::KEYCODE_FN; + let ret = OH_NativeXComponent_GetKeyEventCode(event, &mut std::mem::transmute(code)); + assert!(ret == 0, "Get key event code failed"); + + let keycode = keycodes::translate_keycode(code); + match action { + 0 => send_message(Message::KeyDown { keycode }), + 1 => send_message(Message::KeyUp { keycode }), + _ => (), + } +} + +#[allow(static_mut_refs)] +pub unsafe fn run(conf: crate::conf::Conf, f: F) +where + F: 'static + FnOnce() -> Box, +{ + let env = OHOS_ENV.as_ref().expect("OHOS_ENV is not initialized"); + let exports = OHOS_EXPORTS + .as_ref() + .expect("OHOS_EXPORTS is not initialized"); + let xcomponent = XComponent::init(*env, *exports).expect("Failed to initialize XComponent"); + use std::panic; + panic::set_hook(Box::new(|info| hilog_fatal!(info))); + let _ = register_xcomponent_callbacks(&xcomponent); + set_display_sync(&xcomponent); + struct SendHack(F); + unsafe impl Send for SendHack {} + let f = SendHack(f); + + let (tx, rx) = mpsc::channel(); + + let tx2 = tx.clone(); + MESSAGES_TX.with(move |messages_tx| *messages_tx.borrow_mut() = Some(tx2)); + thread::spawn(move || { + unsafe { + let ret = OH_QoS_SetThreadQoS(QoS_Level_QOS_USER_INTERACTIVE); + if ret < 0 { + hilog_error!(format!("Failed to set thread QoS, ret: {}", ret)); + } else { + hilog_info!("Thread QoS set to USER_INTERACTIVE"); + } + } + let mut libegl = LibEgl::try_load().expect("Cant load LibEGL"); + let window = 'a: loop { + match rx.try_recv() { + Ok(Message::SurfaceCreated { window }) => { + break 'a window; + } + _ => {} + } + }; + let (screen_width, screen_height) = 'a: loop { + match rx.try_recv() { + Ok(Message::SurfaceChanged { width, height }) => { + break 'a (width as f32, height as f32); + } + _ => {} + } + }; + let (egl_context, egl_config, egl_display) = crate::native::egl::create_egl_context( + &mut libegl, + std::ptr::null_mut(), /* EGL_DEFAULT_DISPLAY */ + true, /* force set rgba 8888 for ohos */ + conf.sample_count, + ) + .expect("Cant create EGL context"); + + assert!(!egl_display.is_null()); + assert!(!egl_config.is_null()); + + crate::native::gl::load_gl_funcs(|proc| { + let name = std::ffi::CString::new(proc).unwrap(); + (libegl.eglGetProcAddress)(name.as_ptr() as _) + }); + + let surface = (libegl.eglCreateWindowSurface)( + egl_display, + egl_config, + window.0 as _, + std::ptr::null_mut(), + ); + + if (libegl.eglMakeCurrent)(egl_display, surface, surface, egl_context) == 0 { + panic!(); + } + + let clipboard = Box::new(OHOSClipboard {}); + let tx_fn = Box::new(move |req| tx.send(Message::Request(req)).unwrap()); + crate::set_or_replace_display(NativeDisplayData { + high_dpi: conf.high_dpi, + blocking_event_loop: conf.platform.blocking_event_loop, + ..NativeDisplayData::new(screen_width as _, screen_height as _, tx_fn, clipboard) + }); + let event_handler = f.0(); + let mut s = MainThreadState { + libegl, + egl_display, + egl_config, + egl_context, + surface, + window: WindowRaw(std::ptr::null_mut()), // Will be set when we create the surface + event_handler, + quit: false, + fullscreen: conf.fullscreen, + update_requested: true, + keymods: KeyMods { + shift: false, + ctrl: false, + alt: false, + logo: false, + }, + }; + unsafe { + s.update_surface(window); + if s.surface.is_null() { + hilog_fatal!("Failed to create initial EGL surface"); + return; + } + } + + while !s.quit { + let block_on_wait = conf.platform.blocking_event_loop && !s.update_requested; + + if block_on_wait { + let res = rx.recv(); + + if let Ok(msg) = res { + s.process_message(msg); + } + } else { + // process all the messages from the main thread + while let Ok(msg) = rx.try_recv() { + s.process_message(msg); + } + } + + // Only render if we have a valid surface or if update is requested + if !s.surface.is_null() && (!conf.platform.blocking_event_loop || s.update_requested) { + s.frame(); + } + + thread::yield_now(); + } + + (s.libegl.eglMakeCurrent)( + s.egl_display, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + (s.libegl.eglDestroySurface)(s.egl_display, s.surface); + (s.libegl.eglDestroyContext)(s.egl_display, s.egl_context); + (s.libegl.eglTerminate)(s.egl_display); + }); + + xcomponent.on_surface_created(|xcomponent, win: WindowRaw| { + send_message(Message::SurfaceCreated { window: win }); + let sz = xcomponent.size(win)?; + let width = sz.width as i32; + let height = sz.height as i32; + send_message(Message::SurfaceChanged { width, height }); + Ok(()) + }); + + xcomponent.on_surface_changed(|xcomponent, win| { + let sz = xcomponent.size(win)?; + let width = sz.width as i32; + let height = sz.height as i32; + send_message(Message::SurfaceChanged { width, height }); + Ok(()) + }); + + xcomponent.on_surface_destroyed(|_xcomponent, _win| { + send_message(Message::SurfaceDestroyed); + Ok(()) + }); + xcomponent.on_touch_event(|_xcomponent, _win, data| { + if let Some(touch_point) = data.touch_points.first() { + let phase = match data.event_type { + ohos_xcomponent_binding::TouchEvent::Down => TouchPhase::Started, + ohos_xcomponent_binding::TouchEvent::Up => TouchPhase::Ended, + ohos_xcomponent_binding::TouchEvent::Move => TouchPhase::Moved, + ohos_xcomponent_binding::TouchEvent::Cancel => TouchPhase::Cancelled, + _ => TouchPhase::Cancelled, // Default to cancelled for unknown events + }; + send_message(Message::Touch { + phase, + touch_id: touch_point.id as u64, + x: touch_point.x, + y: touch_point.y, + }); + } + Ok(()) + }); + let _ = xcomponent.register_callback(); + let _ = xcomponent.on_frame_callback(|_, _, _| Ok(())); +} + +pub fn load_file(path: &str, on_loaded: F) { + let response = load_file_sync(path); + on_loaded(response); +} + +fn load_file_sync(path: &str) -> crate::fs::Response { + let full_path = format!("/data/storage/el1/bundle/entry/resources/resfile/{}", path); + match std::fs::read(&full_path) { + Ok(data) => Ok(data), + Err(e) => { + hilog_error!(format!( + "load_file_sync: failed to load file: {} - error: {:?}", + full_path, e + )); + Err(e.into()) + } + } +} + +pub struct OHOSClipboard {} +impl OHOSClipboard { + pub fn new() -> OHOSClipboard { + OHOSClipboard {} + } +} +impl crate::native::Clipboard for OHOSClipboard { + fn get(&mut self) -> Option { + // TODO: not support currently, needs to request permissions. + None + } + fn set(&mut self, _data: &str) { + // TODO: not support currently, needs to request permissions. + } +} diff --git a/src/native/ohos/keycodes.rs b/src/native/ohos/keycodes.rs new file mode 100644 index 000000000..034b68a8f --- /dev/null +++ b/src/native/ohos/keycodes.rs @@ -0,0 +1,107 @@ +use crate::event::KeyCode; +use ohos_input_sys::key_code::Input_KeyCode; + +pub fn translate_keycode(keycode: Input_KeyCode) -> KeyCode { + match keycode { + Input_KeyCode::KEYCODE_0 => KeyCode::Key0, + Input_KeyCode::KEYCODE_1 => KeyCode::Key1, + Input_KeyCode::KEYCODE_2 => KeyCode::Key2, + Input_KeyCode::KEYCODE_3 => KeyCode::Key3, + Input_KeyCode::KEYCODE_4 => KeyCode::Key4, + Input_KeyCode::KEYCODE_5 => KeyCode::Key5, + Input_KeyCode::KEYCODE_6 => KeyCode::Key6, + Input_KeyCode::KEYCODE_7 => KeyCode::Key7, + Input_KeyCode::KEYCODE_8 => KeyCode::Key8, + Input_KeyCode::KEYCODE_9 => KeyCode::Key9, + Input_KeyCode::KEYCODE_DPAD_UP => KeyCode::Up, + Input_KeyCode::KEYCODE_DPAD_DOWN => KeyCode::Down, + Input_KeyCode::KEYCODE_DPAD_LEFT => KeyCode::Left, + Input_KeyCode::KEYCODE_DPAD_RIGHT => KeyCode::Right, + Input_KeyCode::KEYCODE_A => KeyCode::A, + Input_KeyCode::KEYCODE_B => KeyCode::B, + Input_KeyCode::KEYCODE_C => KeyCode::C, + Input_KeyCode::KEYCODE_D => KeyCode::D, + Input_KeyCode::KEYCODE_E => KeyCode::E, + Input_KeyCode::KEYCODE_F => KeyCode::F, + Input_KeyCode::KEYCODE_G => KeyCode::G, + Input_KeyCode::KEYCODE_H => KeyCode::H, + Input_KeyCode::KEYCODE_I => KeyCode::I, + Input_KeyCode::KEYCODE_J => KeyCode::J, + Input_KeyCode::KEYCODE_K => KeyCode::K, + Input_KeyCode::KEYCODE_L => KeyCode::L, + Input_KeyCode::KEYCODE_M => KeyCode::M, + Input_KeyCode::KEYCODE_N => KeyCode::N, + Input_KeyCode::KEYCODE_O => KeyCode::O, + Input_KeyCode::KEYCODE_P => KeyCode::P, + Input_KeyCode::KEYCODE_Q => KeyCode::Q, + Input_KeyCode::KEYCODE_R => KeyCode::R, + Input_KeyCode::KEYCODE_S => KeyCode::S, + Input_KeyCode::KEYCODE_T => KeyCode::T, + Input_KeyCode::KEYCODE_U => KeyCode::U, + Input_KeyCode::KEYCODE_V => KeyCode::V, + Input_KeyCode::KEYCODE_W => KeyCode::W, + Input_KeyCode::KEYCODE_X => KeyCode::X, + Input_KeyCode::KEYCODE_Y => KeyCode::Y, + Input_KeyCode::KEYCODE_Z => KeyCode::Z, + Input_KeyCode::KEYCODE_COMMA => KeyCode::Comma, + Input_KeyCode::KEYCODE_PERIOD => KeyCode::Period, + Input_KeyCode::KEYCODE_ALT_LEFT => KeyCode::LeftAlt, + Input_KeyCode::KEYCODE_ALT_RIGHT => KeyCode::RightAlt, + Input_KeyCode::KEYCODE_SHIFT_LEFT => KeyCode::LeftShift, + Input_KeyCode::KEYCODE_SHIFT_RIGHT => KeyCode::RightShift, + Input_KeyCode::KEYCODE_TAB => KeyCode::Tab, + Input_KeyCode::KEYCODE_SPACE => KeyCode::Space, + Input_KeyCode::KEYCODE_ENTER => KeyCode::Enter, + Input_KeyCode::KEYCODE_DEL => KeyCode::Backspace, + Input_KeyCode::KEYCODE_ESCAPE => KeyCode::Escape, + Input_KeyCode::KEYCODE_PAGE_UP => KeyCode::PageUp, + Input_KeyCode::KEYCODE_PAGE_DOWN => KeyCode::PageDown, + Input_KeyCode::KEYCODE_INSERT => KeyCode::Insert, + Input_KeyCode::KEYCODE_FORWARD_DEL => KeyCode::Delete, + Input_KeyCode::KEYCODE_MOVE_HOME => KeyCode::Home, + Input_KeyCode::KEYCODE_MOVE_END => KeyCode::End, + Input_KeyCode::KEYCODE_F1 => KeyCode::F1, + Input_KeyCode::KEYCODE_F2 => KeyCode::F2, + Input_KeyCode::KEYCODE_F3 => KeyCode::F3, + Input_KeyCode::KEYCODE_F4 => KeyCode::F4, + Input_KeyCode::KEYCODE_F5 => KeyCode::F5, + Input_KeyCode::KEYCODE_F6 => KeyCode::F6, + Input_KeyCode::KEYCODE_F7 => KeyCode::F7, + Input_KeyCode::KEYCODE_F8 => KeyCode::F8, + Input_KeyCode::KEYCODE_F9 => KeyCode::F9, + Input_KeyCode::KEYCODE_F10 => KeyCode::F10, + Input_KeyCode::KEYCODE_F11 => KeyCode::F11, + Input_KeyCode::KEYCODE_F12 => KeyCode::F12, + Input_KeyCode::KEYCODE_CTRL_LEFT => KeyCode::LeftControl, + Input_KeyCode::KEYCODE_CTRL_RIGHT => KeyCode::RightControl, + Input_KeyCode::KEYCODE_CAPS_LOCK => KeyCode::CapsLock, + Input_KeyCode::KEYCODE_NUMPAD_0 => KeyCode::Kp0, + Input_KeyCode::KEYCODE_NUMPAD_1 => KeyCode::Kp1, + Input_KeyCode::KEYCODE_NUMPAD_2 => KeyCode::Kp2, + Input_KeyCode::KEYCODE_NUMPAD_3 => KeyCode::Kp3, + Input_KeyCode::KEYCODE_NUMPAD_4 => KeyCode::Kp4, + Input_KeyCode::KEYCODE_NUMPAD_5 => KeyCode::Kp5, + Input_KeyCode::KEYCODE_NUMPAD_6 => KeyCode::Kp6, + Input_KeyCode::KEYCODE_NUMPAD_7 => KeyCode::Kp7, + Input_KeyCode::KEYCODE_NUMPAD_8 => KeyCode::Kp8, + Input_KeyCode::KEYCODE_NUMPAD_9 => KeyCode::Kp9, + Input_KeyCode::KEYCODE_NUMPAD_DIVIDE => KeyCode::KpDivide, + Input_KeyCode::KEYCODE_NUMPAD_MULTIPLY => KeyCode::KpMultiply, + Input_KeyCode::KEYCODE_NUMPAD_SUBTRACT => KeyCode::KpSubtract, + Input_KeyCode::KEYCODE_NUMPAD_ADD => KeyCode::KpAdd, + Input_KeyCode::KEYCODE_NUMPAD_DOT => KeyCode::KpDecimal, + Input_KeyCode::KEYCODE_NUMPAD_ENTER => KeyCode::KpEnter, + Input_KeyCode::KEYCODE_NUMPAD_EQUALS => KeyCode::KpEqual, + Input_KeyCode::KEYCODE_GRAVE => KeyCode::GraveAccent, + Input_KeyCode::KEYCODE_MINUS => KeyCode::Minus, + Input_KeyCode::KEYCODE_EQUALS => KeyCode::Equal, + Input_KeyCode::KEYCODE_LEFT_BRACKET => KeyCode::LeftBracket, + Input_KeyCode::KEYCODE_RIGHT_BRACKET => KeyCode::RightBracket, + Input_KeyCode::KEYCODE_BACKSLASH => KeyCode::Backslash, + Input_KeyCode::KEYCODE_SEMICOLON => KeyCode::Semicolon, + Input_KeyCode::KEYCODE_APOSTROPHE => KeyCode::Apostrophe, + Input_KeyCode::KEYCODE_SLASH => KeyCode::Slash, + Input_KeyCode::KEYCODE_MENU => KeyCode::Menu, + _ => KeyCode::Unknown, + } +} From 285ae69b634ed4a41ad34cfa3174e778cc82a19b Mon Sep 17 00:00:00 2001 From: ljlvink Date: Sun, 22 Mar 2026 23:58:10 +0800 Subject: [PATCH 3/3] chore: fix clippy --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 5afac1c8c..be6bdeda4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -547,7 +547,9 @@ extern "C" { fn quad_main(); } +#[cfg(target_env = "ohos")] static mut OHOS_EXPORTS: Option> = None; +#[cfg(target_env = "ohos")] static mut OHOS_ENV: Option = None; #[cfg(target_env = "ohos")]