1#![allow(unsafe_op_in_unsafe_fn)]
33#![allow(clippy::not_unsafe_ptr_arg_deref)]
34
35use crate::actor::{Engine, InputEvent, KeyCode};
36use crate::buffer::{Cell, Rgb};
37use crate::layout::Rect;
38use crate::widget::{AppendResult, StreamWidget};
39use std::ffi::CStr;
40use std::os::raw::{c_char, c_int, c_uint};
41use std::ptr;
42
43pub struct FlywheelEngine(Engine);
49
50pub struct FlywheelStream(StreamWidget);
52
53#[repr(C)]
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum FlywheelResult {
61 Ok = 0,
63 NullPointer = 1,
65 InvalidUtf8 = 2,
67 IoError = 3,
69 OutOfBounds = 4,
71 NotRunning = 5,
73}
74
75#[repr(C)]
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum FlywheelEventType {
79 None = 0,
81 Key = 1,
83 Resize = 2,
85 Error = 3,
87 Shutdown = 4,
89}
90
91#[repr(C)]
93#[derive(Debug, Clone, Copy)]
94pub struct FlywheelKeyEvent {
95 pub char_code: u32,
97 pub key_code: c_int,
99 pub modifiers: c_uint,
101}
102
103#[repr(C)]
105#[derive(Debug, Clone, Copy)]
106pub struct FlywheelResizeEvent {
107 pub width: u16,
109 pub height: u16,
111}
112
113#[repr(C)]
115pub struct FlywheelEvent {
116 pub event_type: FlywheelEventType,
118 pub key: FlywheelKeyEvent,
120 pub resize: FlywheelResizeEvent,
122}
123
124pub const FLYWHEEL_KEY_NONE: c_int = 0;
127pub const FLYWHEEL_KEY_ENTER: c_int = 1;
129pub const FLYWHEEL_KEY_ESCAPE: c_int = 2;
131pub const FLYWHEEL_KEY_BACKSPACE: c_int = 3;
133pub const FLYWHEEL_KEY_TAB: c_int = 4;
135pub const FLYWHEEL_KEY_LEFT: c_int = 5;
137pub const FLYWHEEL_KEY_RIGHT: c_int = 6;
139pub const FLYWHEEL_KEY_UP: c_int = 7;
141pub const FLYWHEEL_KEY_DOWN: c_int = 8;
143pub const FLYWHEEL_KEY_HOME: c_int = 9;
145pub const FLYWHEEL_KEY_END: c_int = 10;
147pub const FLYWHEEL_KEY_PAGE_UP: c_int = 11;
149pub const FLYWHEEL_KEY_PAGE_DOWN: c_int = 12;
151pub const FLYWHEEL_KEY_DELETE: c_int = 13;
153
154pub const FLYWHEEL_MOD_SHIFT: c_uint = 1;
157pub const FLYWHEEL_MOD_CTRL: c_uint = 2;
159pub const FLYWHEEL_MOD_ALT: c_uint = 4;
161pub const FLYWHEEL_MOD_SUPER: c_uint = 8;
163
164#[unsafe(no_mangle)]
172pub extern "C" fn flywheel_engine_new() -> *mut FlywheelEngine {
173 Engine::new().map_or(
174 ptr::null_mut(),
175 |engine| Box::into_raw(Box::new(FlywheelEngine(engine)))
176 )
177}
178
179#[unsafe(no_mangle)]
181pub unsafe extern "C" fn flywheel_engine_destroy(engine: *mut FlywheelEngine) {
182 if !engine.is_null() {
183 drop(Box::from_raw(engine));
184 }
185}
186
187#[unsafe(no_mangle)]
189pub const unsafe extern "C" fn flywheel_engine_width(engine: *const FlywheelEngine) -> u16 {
190 if engine.is_null() {
191 return 0;
192 }
193 (*engine).0.width()
194}
195
196#[unsafe(no_mangle)]
198pub const unsafe extern "C" fn flywheel_engine_height(engine: *const FlywheelEngine) -> u16 {
199 if engine.is_null() {
200 return 0;
201 }
202 (*engine).0.height()
203}
204
205#[unsafe(no_mangle)]
207pub const unsafe extern "C" fn flywheel_engine_is_running(engine: *const FlywheelEngine) -> bool {
208 if engine.is_null() {
209 return false;
210 }
211 (*engine).0.is_running()
212}
213
214#[unsafe(no_mangle)]
216pub unsafe extern "C" fn flywheel_engine_stop(engine: *mut FlywheelEngine) {
217 if !engine.is_null() {
218 (*engine).0.stop();
219 }
220}
221
222#[unsafe(no_mangle)]
224pub unsafe extern "C" fn flywheel_engine_poll_event(
225 engine: *const FlywheelEngine,
226 event_out: *mut FlywheelEvent,
227) -> FlywheelEventType {
228 if engine.is_null() || event_out.is_null() {
229 return FlywheelEventType::None;
230 }
231
232 match (*engine).0.poll_input() {
233 Some(InputEvent::Key { code, modifiers }) => {
234 let (char_code, key_code) = convert_key_code(code);
235 let mods = convert_modifiers(modifiers);
236
237 (*event_out).event_type = FlywheelEventType::Key;
238 (*event_out).key = FlywheelKeyEvent {
239 char_code,
240 key_code,
241 modifiers: mods,
242 };
243 FlywheelEventType::Key
244 }
245 Some(InputEvent::Resize { width, height }) => {
246 (*event_out).event_type = FlywheelEventType::Resize;
247 (*event_out).resize = FlywheelResizeEvent { width, height };
248 FlywheelEventType::Resize
249 }
250 Some(InputEvent::Shutdown) => {
251 (*event_out).event_type = FlywheelEventType::Shutdown;
252 FlywheelEventType::Shutdown
253 }
254 Some(InputEvent::Error(_)) => {
255 (*event_out).event_type = FlywheelEventType::Error;
256 FlywheelEventType::Error
257 }
258 _ => {
259 (*event_out).event_type = FlywheelEventType::None;
260 FlywheelEventType::None
261 }
262 }
263}
264
265#[unsafe(no_mangle)]
267pub unsafe extern "C" fn flywheel_engine_handle_resize(
268 engine: *mut FlywheelEngine,
269 width: u16,
270 height: u16,
271) {
272 if !engine.is_null() {
273 (*engine).0.handle_resize(width, height);
274 }
275}
276
277#[unsafe(no_mangle)]
279pub unsafe extern "C" fn flywheel_engine_request_redraw(engine: *const FlywheelEngine) {
280 if !engine.is_null() {
281 (*engine).0.request_redraw();
282 }
283}
284
285#[unsafe(no_mangle)]
287pub unsafe extern "C" fn flywheel_engine_request_update(engine: *const FlywheelEngine) {
288 if !engine.is_null() {
289 (*engine).0.request_update();
290 }
291}
292
293#[unsafe(no_mangle)]
295pub unsafe extern "C" fn flywheel_engine_begin_frame(engine: *mut FlywheelEngine) {
296 if !engine.is_null() {
297 (*engine).0.begin_frame();
298 }
299}
300
301#[unsafe(no_mangle)]
303pub unsafe extern "C" fn flywheel_engine_end_frame(engine: *mut FlywheelEngine) {
304 if !engine.is_null() {
305 (*engine).0.end_frame();
306 }
307}
308
309#[unsafe(no_mangle)]
311#[allow(clippy::cast_sign_loss)] pub unsafe extern "C" fn flywheel_engine_set_cell(
313 engine: *mut FlywheelEngine,
314 x: u16,
315 y: u16,
316 c: c_char,
317 fg: u32,
318 bg: u32,
319) {
320 if engine.is_null() {
321 return;
322 }
323 let cell = Cell::new(c as u8 as char)
324 .with_fg(Rgb::from_u32(fg))
325 .with_bg(Rgb::from_u32(bg));
326 (*engine).0.set_cell(x, y, cell);
327}
328
329#[unsafe(no_mangle)]
331pub unsafe extern "C" fn flywheel_engine_draw_text(
332 engine: *mut FlywheelEngine,
333 x: u16,
334 y: u16,
335 text: *const c_char,
336 fg: u32,
337 bg: u32,
338) -> u16 {
339 if engine.is_null() || text.is_null() {
340 return 0;
341 }
342
343 let Ok(text_str) = CStr::from_ptr(text).to_str() else {
344 return 0;
345 };
346
347 (*engine)
348 .0
349 .draw_text(x, y, text_str, Rgb::from_u32(fg), Rgb::from_u32(bg))
350}
351
352#[unsafe(no_mangle)]
354pub unsafe extern "C" fn flywheel_engine_clear(engine: *mut FlywheelEngine) {
355 if !engine.is_null() {
356 (*engine).0.clear();
357 }
358}
359
360#[unsafe(no_mangle)]
362#[allow(clippy::cast_sign_loss)] pub unsafe extern "C" fn flywheel_engine_fill_rect(
364 engine: *mut FlywheelEngine,
365 x: u16,
366 y: u16,
367 width: u16,
368 height: u16,
369 c: c_char,
370 fg: u32,
371 bg: u32,
372) {
373 if engine.is_null() {
374 return;
375 }
376 let cell = Cell::new(c as u8 as char)
377 .with_fg(Rgb::from_u32(fg))
378 .with_bg(Rgb::from_u32(bg));
379 (*engine).0.fill_rect(Rect::new(x, y, width, height), cell);
380}
381
382#[unsafe(no_mangle)]
388pub extern "C" fn flywheel_stream_new(x: u16, y: u16, width: u16, height: u16) -> *mut FlywheelStream {
389 let widget = StreamWidget::new(Rect::new(x, y, width, height));
390 Box::into_raw(Box::new(FlywheelStream(widget)))
391}
392
393#[unsafe(no_mangle)]
395pub unsafe extern "C" fn flywheel_stream_destroy(stream: *mut FlywheelStream) {
396 if !stream.is_null() {
397 drop(Box::from_raw(stream));
398 }
399}
400
401#[unsafe(no_mangle)]
403pub unsafe extern "C" fn flywheel_stream_append(
404 stream: *mut FlywheelStream,
405 text: *const c_char,
406) -> c_int {
407 if stream.is_null() || text.is_null() {
408 return -1;
409 }
410
411 let Ok(text_str) = CStr::from_ptr(text).to_str() else {
412 return -1;
413 };
414
415 match (*stream).0.append(text_str) {
416 AppendResult::FastPath { .. } => 1,
417 AppendResult::SlowPath { .. } | AppendResult::Empty => 0,
418 }
419}
420
421#[unsafe(no_mangle)]
423pub unsafe extern "C" fn flywheel_stream_render(
424 stream: *mut FlywheelStream,
425 engine: *mut FlywheelEngine,
426) {
427 if stream.is_null() || engine.is_null() {
428 return;
429 }
430 (*stream).0.render((*engine).0.buffer_mut());
431}
432
433#[unsafe(no_mangle)]
435pub unsafe extern "C" fn flywheel_stream_clear(stream: *mut FlywheelStream) {
436 if !stream.is_null() {
437 (*stream).0.clear();
438 }
439}
440
441#[unsafe(no_mangle)]
443pub unsafe extern "C" fn flywheel_stream_set_fg(stream: *mut FlywheelStream, color: u32) {
444 if !stream.is_null() {
445 (*stream).0.set_fg(Rgb::from_u32(color));
446 }
447}
448
449#[unsafe(no_mangle)]
451pub unsafe extern "C" fn flywheel_stream_set_bg(stream: *mut FlywheelStream, color: u32) {
452 if !stream.is_null() {
453 (*stream).0.set_bg(Rgb::from_u32(color));
454 }
455}
456
457#[unsafe(no_mangle)]
459pub unsafe extern "C" fn flywheel_stream_scroll_up(stream: *mut FlywheelStream, lines: usize) {
460 if !stream.is_null() {
461 (*stream).0.scroll_up(lines);
462 }
463}
464
465#[unsafe(no_mangle)]
467pub unsafe extern "C" fn flywheel_stream_scroll_down(stream: *mut FlywheelStream, lines: usize) {
468 if !stream.is_null() {
469 (*stream).0.scroll_down(lines);
470 }
471}
472
473#[unsafe(no_mangle)]
479#[allow(clippy::cast_lossless)]
480pub const extern "C" fn flywheel_rgb(r: u8, g: u8, b: u8) -> u32 {
481 ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
482}
483
484#[unsafe(no_mangle)]
490pub extern "C" fn flywheel_version() -> *const c_char {
491 static VERSION: &[u8] = b"0.1.0\0";
492 VERSION.as_ptr().cast::<c_char>()
493}
494
495const fn convert_key_code(code: KeyCode) -> (u32, c_int) {
500 match code {
501 KeyCode::Char(c) => (c as u32, FLYWHEEL_KEY_NONE),
502 KeyCode::Enter => (0, FLYWHEEL_KEY_ENTER),
503 KeyCode::Esc => (0, FLYWHEEL_KEY_ESCAPE),
504 KeyCode::Backspace => (0, FLYWHEEL_KEY_BACKSPACE),
505 KeyCode::Tab => (0, FLYWHEEL_KEY_TAB),
506 KeyCode::Left => (0, FLYWHEEL_KEY_LEFT),
507 KeyCode::Right => (0, FLYWHEEL_KEY_RIGHT),
508 KeyCode::Up => (0, FLYWHEEL_KEY_UP),
509 KeyCode::Down => (0, FLYWHEEL_KEY_DOWN),
510 KeyCode::Home => (0, FLYWHEEL_KEY_HOME),
511 KeyCode::End => (0, FLYWHEEL_KEY_END),
512 KeyCode::PageUp => (0, FLYWHEEL_KEY_PAGE_UP),
513 KeyCode::PageDown => (0, FLYWHEEL_KEY_PAGE_DOWN),
514 KeyCode::Delete => (0, FLYWHEEL_KEY_DELETE),
515 _ => (0, FLYWHEEL_KEY_NONE),
516 }
517}
518
519const fn convert_modifiers(mods: crate::actor::KeyModifiers) -> c_uint {
520 let mut result = 0;
521 if mods.shift {
522 result |= FLYWHEEL_MOD_SHIFT;
523 }
524 if mods.control {
525 result |= FLYWHEEL_MOD_CTRL;
526 }
527 if mods.alt {
528 result |= FLYWHEEL_MOD_ALT;
529 }
530 if mods.super_key {
531 result |= FLYWHEEL_MOD_SUPER;
532 }
533 result
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_flywheel_rgb() {
542 assert_eq!(flywheel_rgb(255, 128, 64), 0xFF8040);
543 assert_eq!(flywheel_rgb(0, 0, 0), 0x000000);
544 assert_eq!(flywheel_rgb(255, 255, 255), 0xFFFFFF);
545 }
546
547 #[test]
548 fn test_flywheel_version() {
549 unsafe {
550 let version = flywheel_version();
551 let version_str = CStr::from_ptr(version).to_str().unwrap();
552 assert_eq!(version_str, "0.1.0");
553 }
554 }
555}