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