Speedy2D glutwindow draw callback
First of all, thanks for all the videos, examples and making fltk-rs!
I've been trying to learn Rust and write an application that uses FLTK and Speedy2D. Currently on the Speedy2D page the instructions for managing GL oneself is this:
let mut renderer = unsafe {
GLRenderer::new_for_gl_context((640, 480), |fn_name| {
window_context.get_proc_address(fn_name) as *const _
})
which is a bit different than what is in the fltk/Speedy2D demo. But I've managed to make a program run with this in Cargo.toml:
edition = "2024"
[dependencies]
fltk = { version = "^1.5", features = ["enable-glwindow"] }
speedy2d = { version = "2.1.0", default-features = false }
The problem is writing the glutwindow draw() function if it needs to move in data from a struct owned by self. A custom draw_on_glut(&mut self) function, which invokes Speedy2D, somewhat works but it's not always called when needed. I am unsure if packing the variables in std::rc, extending the glutwindow widget somehow or using a different layout is the way to go.
This example shows the structure I am currently at:
use fltk::{app::{self, event_key, Receiver, Sender}, enums::{Event, Key}, prelude::*, window::{self, Window}};
use speedy2d::dimen::Vector2;
fn main() {
let args: Vec<_> = std::env::args().collect();
let mut app = MyApp::build(args);
app.launch();
}
pub enum Message {
Up,
Down,
Left,
Right,
Reset,
GlutResized,
}
pub struct MyApp {
app: app::App,
tx: Sender<Message>,
rx: Receiver<Message>,
display: MyDisplay,
}
impl MyApp {
pub fn build(args: Vec<String>) -> Self {
let (tx, rx) = app::channel::<Message>();
let app = app::App::default();
let mut main_win = Window::default().with_size(800, 600);
main_win.make_resizable(true);
let glut_win = fltk::window::GlutWindow::default_fill();
glut_win.end();
main_win.end();
main_win.show();
let mut display: MyDisplay = MyDisplay::build(glut_win, tx);
display.draw_on_glut();
Self {
app,
tx,
rx,
display,
}
}
pub fn launch(&mut self) {
while self.app.wait() {
use Message::*;
if let Some(msg) = self.rx.recv() {
match msg {
Up => self.display.up(),
Down => self.display.down(),
Left => self.display.left(),
Right => self.display.right(),
Reset => self.display.reset(),
GlutResized => self.display.resize_glut(),
}
}
}
}
}
pub struct MyDisplay {
glut_win: window::GlutWindow,
renderer: speedy2d::GLRenderer,
xpos: f32, //example of field in struct used to draw circle
ypos: f32,
}
impl MyDisplay {
pub fn build(mut glut_win: fltk::window::GlutWindow, tx: app::Sender<Message>) -> Self {
let w = glut_win.width();
let h = glut_win.height();
let renderer = unsafe {
speedy2d::GLRenderer::new_for_gl_context((w as u32, h as u32), |fn_name| {
glut_win.get_proc_address(fn_name) as *const _
})}.expect("cannot connect glcontext");
glut_win.resize_callback(move|_sel, _xpos, _ypos, _wid, _hei| {
tx.send(Message::GlutResized);
});
glut_win.draw(|_| {
//make this work somehow
//have speedy2d renderer, graphics, draw_circle in here?
});
glut_win.handle(move |widget, event| {
match event {
Event::Focus => {
true
},
Event::KeyDown => {
match event_key() {
Key::Up => {
tx.send(Message::Up);
true
},
Key::Down => {
tx.send(Message::Down);
true
},
Key::Left => {
tx.send(Message::Left);
true
},
Key::Right => {
tx.send(Message::Right);
true
},
Key::Enter => {
tx.send(Message::Reset);
true
},
_ => false,
}
},
_ => false,
}
});
Self {
glut_win,
renderer,
xpos: 200.,
ypos: 100.,
}
}
pub fn resize_glut(&mut self) {
self.draw_on_glut();
}
pub fn draw_on_glut(&mut self) {
self.renderer.set_viewport_size_pixels(Vector2::new(self.glut_win.width() as u32, self.glut_win.height() as u32));
self.renderer.draw_frame(|graphics| {
graphics.clear_screen(speedy2d::color::Color::DARK_GRAY);
graphics.draw_circle((self.xpos, self.ypos), 50., speedy2d::color::Color::RED);
});
self.glut_win.flush(); //seems to need damage or flush to draw...
//self.glut_win.set_damage(true);
}
pub fn reset(&mut self) {
self.xpos = 300.;
self.ypos = 60.;
self.draw_on_glut();
}
pub fn up(&mut self) {
self.ypos -= 1.;
self.draw_on_glut();
}
pub fn down(&mut self) {
self.ypos += 1.;
self.draw_on_glut();
}
pub fn left(&mut self) {
self.xpos -= 1.;
self.draw_on_glut();
}
pub fn right(&mut self) {
self.xpos += 1.;
self.draw_on_glut();
}
}
Hello Thank you for the kind words. For realtime drawing, it's better not having it be driven by app.wait() and messages. The event loop will be limited to event firing and a message queue, which might cause perf issues. I've modified the code to invoke speedy2D's renderer in the GlutWindow's draw method, while invoking a redraw with events fired in the handle method, and avoided using messages:
use fltk::{app::{self, event_key, Receiver, Sender}, enums::{Event, Key}, prelude::*, window::{self, Window}};
use speedy2d::dimen::Vector2;
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let args: Vec<_> = std::env::args().collect();
let mut app = MyApp::build(args);
app.launch();
}
pub struct MyApp {
app: app::App,
display: MyDisplay,
}
impl MyApp {
pub fn build(args: Vec<String>) -> Self {
let app = app::App::default();
let mut main_win = Window::default().with_size(800, 600);
main_win.make_resizable(true);
let glut_win = fltk::window::GlutWindow::default_fill();
glut_win.end();
main_win.end();
main_win.show();
let mut display: MyDisplay = MyDisplay::build(glut_win);
Self {
app,
display,
}
}
pub fn launch(&mut self) {
self.app.run().unwrap();
}
}
pub struct MyDisplay {
glut_win: window::GlutWindow,
}
impl MyDisplay {
pub fn build(mut glut_win: fltk::window::GlutWindow) -> Self {
let w = glut_win.width();
let h = glut_win.height();
let mut renderer = unsafe {
speedy2d::GLRenderer::new_for_gl_context((w as u32, h as u32), |fn_name| {
glut_win.get_proc_address(fn_name) as *const _
})}.expect("cannot connect glcontext");
let xpos = Rc::from(RefCell::from(200.));
let ypos = Rc::from(RefCell::from(100.));
glut_win.draw({
let xpos = xpos.clone();
let ypos = ypos.clone();
move |w| {
renderer.set_viewport_size_pixels(Vector2::new(w.width() as u32, w.height() as u32));
renderer.draw_frame(|graphics| {
graphics.clear_screen(speedy2d::color::Color::DARK_GRAY);
graphics.draw_circle((*xpos.borrow(), *ypos.borrow()), 50., speedy2d::color::Color::RED);
});
}});
glut_win.handle(move |widget, event| {
match event {
Event::Focus => {
true
},
Event::KeyDown => {
match event_key() {
Key::Up => {
*ypos.borrow_mut() -= 1.;
widget.redraw();
true
},
Key::Down => {
*ypos.borrow_mut() += 1.;
widget.redraw();
true
},
Key::Left => {
*xpos.borrow_mut() -= 1.;
widget.redraw();
true
},
Key::Right => {
*xpos.borrow_mut() += 1.;
widget.redraw();
true
},
Key::Enter => {
*xpos.borrow_mut() = 300.;
*ypos.borrow_mut() = 60.;
widget.redraw();
true
},
_ => false,
}
},
_ => false,
}
});
Self {
glut_win,
}
}
}
This also removes some of the methods like reset, up etc. You can refactor them if needed into a Circle struct which you can also wrap in a Rc RefCell, so you can call for example circle.up() in the GlutWindow's handle.
Yay. Now draw() seems to work as intended. I will need to read up on Rc and RefCell.
While this is not for a game. At some point it would be nice to have smooth scrolling.
Initial, perhaps naive, tests with:
*ypos.borrow_mut() -= 1.; widget.redraw();
In a loop.
Only resulted in choppy motion, even with thread sleep after the redraw() call.
Is app.run() not suited for this?
Edit: Come to Think of it. I should test more with swap_buffers, flush, damage and the like.
Most OSes likely throttle key presses the first time, then detect continued presses. If you try the following program:
use fltk::{app::{self, event_key, Receiver, Sender}, enums::{Event, Key}, prelude::*, window::{self, Window}};
use speedy2d::dimen::Vector2;
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let args: Vec<_> = std::env::args().collect();
let mut app = MyApp::build(args);
app.launch();
}
pub struct MyApp {
app: app::App,
display: MyDisplay,
}
impl MyApp {
pub fn build(args: Vec<String>) -> Self {
let app = app::App::default();
let mut main_win = Window::default().with_size(800, 600);
main_win.make_resizable(true);
let glut_win = fltk::window::GlutWindow::default_fill();
glut_win.end();
main_win.end();
main_win.show();
let mut display: MyDisplay = MyDisplay::build(glut_win);
Self {
app,
display,
}
}
pub fn launch(&mut self) {
self.app.run().unwrap();
}
}
pub struct MyDisplay {
glut_win: window::GlutWindow,
}
impl MyDisplay {
pub fn build(mut glut_win: fltk::window::GlutWindow) -> Self {
let w = glut_win.width();
let h = glut_win.height();
let mut renderer = unsafe {
speedy2d::GLRenderer::new_for_gl_context((w as u32, h as u32), |fn_name| {
glut_win.get_proc_address(fn_name) as *const _
})}.expect("cannot connect glcontext");
let xpos = Rc::from(RefCell::from(200.));
let ypos = Rc::from(RefCell::from(100.));
glut_win.draw({
let xpos = xpos.clone();
let ypos = ypos.clone();
move |w| {
renderer.set_viewport_size_pixels(Vector2::new(w.width() as u32, w.height() as u32));
renderer.draw_frame(|graphics| {
graphics.clear_screen(speedy2d::color::Color::DARK_GRAY);
graphics.draw_circle((*xpos.borrow(), *ypos.borrow()), 50., speedy2d::color::Color::RED);
});
}});
let current_key: Rc<RefCell<Key>> = Rc::new(RefCell::new(Key::None));
app::add_idle3({
let mut widget = glut_win.clone();
let current_key = current_key.clone();
move |_| {
match *current_key.borrow() {
Key::Up => *ypos.borrow_mut() -= 5.,
Key::Down => *ypos.borrow_mut() += 5.,
Key::Left => *xpos.borrow_mut() -= 5.,
Key::Right => *xpos.borrow_mut() += 5.,
Key::Enter => {
*xpos.borrow_mut() = 300.;
*ypos.borrow_mut() = 60.;
},
_ => (),
}
widget.redraw();
app::sleep(0.016);
}});
glut_win.handle(move |widget, event| {
match event {
Event::Focus => {
true
},
Event::KeyUp => {
*current_key.borrow_mut() = Key::None;
true
}
Event::KeyDown => {
match event_key() {
Key::Up => {
*current_key.borrow_mut() = Key::Up;
true
},
Key::Down => {
*current_key.borrow_mut() = Key::Down;
true
},
Key::Left => {
*current_key.borrow_mut() = Key::Left;
true
},
Key::Right => {
*current_key.borrow_mut() = Key::Right;
true
},
Key::Enter => {
*current_key.borrow_mut() = Key::Enter;
true
},
_ => false,
}
},
_ => false,
}
});
Self {
glut_win,
}
}
}
The app::add_idle hooks into the event loop and runs continuously, we tell it to sleep for 16 milliseconds. The keys are stored from the glut_win handler and are handled in add_idle. This works around the throttiling.
Thank you very much. Rc and RefCell makes more sense to me now. I guess app::add_idle should only be used for simple operations and I hope it avoids any borrowing panics by hooking into the eventloop single threaded. I will have a go with an approach like this:
use fltk::{app::{self, event_key, event_key_down, Receiver, Sender}, enums::{Event, Key}, prelude::*, window::{self, Window}};
use speedy2d::dimen::Vector2;
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let args: Vec<_> = std::env::args().collect();
let mut app = MyApp::build(args);
app.launch();
}
pub struct MyApp {
app: app::App,
display: MyDisplay,
}
impl MyApp {
pub fn build(args: Vec<String>) -> Self {
let app = app::App::default();
let mut main_win = Window::default().with_size(800, 600);
main_win.make_resizable(true);
let glut_win = fltk::window::GlutWindow::default_fill();
glut_win.end();
main_win.end();
main_win.show();
let mut display: MyDisplay = MyDisplay::build(glut_win);
Self {
app,
display,
}
}
pub fn launch(&mut self) {
self.app.run().unwrap();
}
}
pub struct MyDisplay {
glut_win: window::GlutWindow,
}
impl MyDisplay {
pub fn build(mut glut_win: fltk::window::GlutWindow) -> Self {
let w = glut_win.width();
let h = glut_win.height();
let mut renderer = unsafe {
speedy2d::GLRenderer::new_for_gl_context((w as u32, h as u32), |fn_name| {
glut_win.get_proc_address(fn_name) as *const _
})}.expect("cannot connect glcontext");
let xpos = Rc::from(RefCell::from(200.));
let ypos = Rc::from(RefCell::from(100.));
glut_win.draw({
let xpos = xpos.clone();
let ypos = ypos.clone();
move |w| {
renderer.set_viewport_size_pixels(Vector2::new(w.width() as u32, w.height() as u32));
renderer.draw_frame(|graphics| {
graphics.clear_screen(speedy2d::color::Color::DARK_GRAY);
graphics.draw_circle((*xpos.borrow(), *ypos.borrow()), 50., speedy2d::color::Color::RED);
});
}});
let movement_vec: Rc<RefCell<Vector2<f32>>> = Rc::new(RefCell::new(Vector2::new(0.,0.)));
let mut move_refresh: Option<*mut ()> = None;
glut_win.handle(move |widget, event| {
match event {
Event::Focus => {
true
},
Event::KeyUp | Event::KeyDown => {
match event_key() {
Key::Up | Key::Down | Key::Left | Key::Right => {
if !event_key_down(Key::Up)
&& !event_key_down(Key::Down)
&& !event_key_down(Key::Left)
&& !event_key_down(Key::Right) {
if let Some(handle) = move_refresh {
app::remove_idle3(handle);
move_refresh = None;
}
} else {
movement_vec.borrow_mut().x = 0.;
movement_vec.borrow_mut().y = 0.;
if event_key_down(Key::Up) {
movement_vec.borrow_mut().y -= 1.;
}
if event_key_down(Key::Down) {
movement_vec.borrow_mut().y += 1.;
}
if event_key_down(Key::Left) {
movement_vec.borrow_mut().x -= 1.;
}
if event_key_down(Key::Right) {
movement_vec.borrow_mut().x += 1.;
}
if !event_key_down(Key::ShiftL) && !event_key_down(Key::ShiftR){
movement_vec.borrow_mut().x *= 3.;
movement_vec.borrow_mut().y *= 3.;
}
if event_key_down(Key::ControlL) || event_key_down(Key::ControlR) {
movement_vec.borrow_mut().x *= 2.;
movement_vec.borrow_mut().y *= 2.;
}
if movement_vec.borrow().x != 0. && movement_vec.borrow().y != 0. {
movement_vec.borrow_mut().x *= 0.7;
movement_vec.borrow_mut().y *= 0.7;
}
if move_refresh.is_none() {
move_refresh =
Some(app::add_idle3({
let mut widget = widget.clone();
let m_v = movement_vec.clone();
let xpos = xpos.clone();
let ypos = ypos.clone();
move |_| {
*xpos.borrow_mut() += m_v.borrow().x;
*ypos.borrow_mut() += m_v.borrow().y;
widget.redraw();
app::sleep(0.007);
}
}));
}
}
true
},
_ => false,
}
},
_ => false,
}
});
Self {
glut_win,
}
}
}
It makes it possible to slow down/speed up the movement by holding Shift or Ctrl. Maybe the movement vector approach will be useful later to add mouse dragging. I guess remove_idle can save some cycles when showing a static image.
Edit: I feared mouse dragging might have to involve movement deltas over some time period and a refresh firing. But from the FLTK Rust book it seems simple if it's possible to redraw on every drag event with something like this:
...
let mut click_coords = (0, 0);
let mut start_pos = (0., 0.);
glut_win.handle(move |widget, event| {
match event {
Event::Push => {
click_coords = app::event_coords();
start_pos = (*xpos.borrow(), *ypos.borrow());
true
}
Event::Drag => {
let dx = app::event_coords().0 - click_coords.0;
let dy = app::event_coords().1 - click_coords.1;
*xpos.borrow_mut() = start_pos.0 + (dx) as f32;
*ypos.borrow_mut() = start_pos.1 + (dy) as f32;
widget.redraw();
true
}
...
Edit2: Unfortunately multiple add_ and remove_idle made the Rc::strong_count pile up. I ended up keeping the permanent idle hook and put widget.redraw() inside an if-block.
Edit3: On Windows it seems the Event::Drag is restricted to fire in between or in app::sleep durations. On x11 they seem decoupled.