Mercurial > tablet-emu
changeset 18:3f7b7a3ad8fe
Build three binaries instead of using arguments.
| author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
|---|---|
| date | Thu, 26 Aug 2021 22:15:55 +0200 |
| parents | 0bce7fe96937 |
| children | ba09079686a0 |
| files | Cargo.toml src/bin/client.rs src/bin/server.rs src/bin/standalone.rs src/client.rs src/gtk.rs src/lib.rs src/main.rs src/protocol.rs src/server.rs |
| diffstat | 10 files changed, 594 insertions(+), 607 deletions(-) [+] |
line wrap: on
line diff
--- a/Cargo.toml Thu Aug 26 22:00:55 2021 +0200 +++ b/Cargo.toml Thu Aug 26 22:15:55 2021 +0200 @@ -24,3 +24,15 @@ [profile.release] lto = true + +[[bin]] +name = "tablet-emu" +path = "src/bin/standalone.rs" + +[[bin]] +name = "tablet-emud" +path = "src/bin/server.rs" + +[[bin]] +name = "tablet-emu-remote" +path = "src/bin/client.rs"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/bin/client.rs Thu Aug 26 22:15:55 2021 +0200 @@ -0,0 +1,212 @@ +// Tablet emulator, for people who don’t own one +// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use gio::prelude::*; +use glib::clone; +use gtk::prelude::*; +use input_linux::Key; +use std::net::UdpSocket; +use std::sync::{Arc, Mutex}; +use tablet_emu::protocol::{Buttons, Event}; +use tablet_emu::{state::FakeState, DEFAULT_HEIGHT, DEFAULT_WIDTH}; + +fn build_main_menu(app: >k::Application) { + let quit = gio::SimpleAction::new("quit", None); + app.set_accels_for_action("app.quit", &["<Control>q"]); + app.add_action(&quit); + quit.connect_activate(clone!(@weak app => move |_, _| app.quit())); + + let about = gio::SimpleAction::new("about", None); + app.add_action(&about); + about.connect_activate(|_, _| { + let about = gtk::AboutDialog::builder() + .program_name("TabletEmu") + .logo_icon_name("input-tablet") + .website("https://hg.linkmauve.fr/tablet-emu") + .version("0.1") + .license_type(gtk::License::Agpl30) + .copyright("© 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>") + .build(); + //about.run(); + about.destroy(); + }); + + let menu = gio::Menu::new(); + { + let file = gio::Menu::new(); + file.append(Some("_Quit"), Some("app.quit")); + menu.append_submenu(Some("_File"), &file); + } + { + let help = gio::Menu::new(); + help.append(Some("_About"), Some("app.about")); + menu.append_submenu(Some("_Help"), &help); + } + app.set_menubar(Some(&menu)); +} + +fn build_ui(app: >k::Application) { + build_main_menu(app); + + let address = "127.0.0.1:16150"; + let socket = Arc::new(Mutex::new(UdpSocket::bind("127.0.0.1:4321").unwrap())); + println!("opened {:?}", socket); + + let send = move |event: &Event| { + // TODO: Yolo-alignment. + let buf: &[u8; std::mem::size_of::<Event>()] = unsafe { std::mem::transmute(event) }; + let socket = socket.lock().unwrap(); + let amount = socket.send_to(buf, address).unwrap(); + assert_eq!(amount, std::mem::size_of::<Event>()); + }; + let send2 = send.clone(); + let send3 = send.clone(); + let send4 = send.clone(); + + let state = Arc::new(Mutex::new(FakeState::new())); + + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + let tools_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + + macro_rules! impl_tool { + ($tool:tt) => { + let tool = gtk::Button::with_mnemonic($tool); + let state_weak = Arc::downgrade(&state); + let send = send.clone(); + tool.connect_clicked(move |b| { + let state = state_weak.upgrade().unwrap(); + let mut state = state.lock().unwrap(); + let tool = match b.label().unwrap().as_str() { + "_Pen" => Key::ButtonToolPen, + "_Rubber" => Key::ButtonToolRubber, + "_Brush" => Key::ButtonToolBrush, + "P_encil" => Key::ButtonToolPencil, + "_Airbrush" => Key::ButtonToolAirbrush, + _ => unreachable!(), + }; + state.select_tool(tool); + + let mut event: Event = Default::default(); + event.buttons = match tool { + Key::ButtonToolPen => Buttons::A, + Key::ButtonToolRubber => Buttons::B, + Key::ButtonToolBrush => Buttons::X, + Key::ButtonToolPencil => Buttons::Y, + Key::ButtonToolAirbrush => Buttons::SELECT, + _ => unreachable!(), + }; + send(&event); + }); + tools_box.append(&tool); + }; + } + impl_tool!("_Pen"); + impl_tool!("_Rubber"); + impl_tool!("_Brush"); + impl_tool!("P_encil"); + impl_tool!("_Airbrush"); + + let drawing_area = gtk::DrawingArea::builder() + .content_width(DEFAULT_WIDTH) + .content_height(DEFAULT_HEIGHT) + .hexpand(true) + .build(); + let gesture_click = gtk::GestureClick::new(); + let event_controller = gtk::EventControllerMotion::new(); + let state_weak = Arc::downgrade(&state); + drawing_area.connect_resize(move |_, width, height| { + let state = state_weak.upgrade().unwrap(); + let mut state = state.lock().unwrap(); + state.set_size(width, height); + + let mut event: Event = Default::default(); + event.buttons = Buttons::RESIZE; + event.touch = (width as u16, height as u16); + send(&event); + }); + let state_weak = Arc::downgrade(&state); + gesture_click.connect_pressed(move |_, n_press, x, y| { + if n_press != 1 { + return; + } + + let state = state_weak.upgrade().unwrap(); + let mut state = state.lock().unwrap(); + state.press(x, y).unwrap(); + + let mut event: Event = Default::default(); + event.buttons = Buttons::TOUCH; + event.touch = (x as u16, y as u16); + send2(&event); + }); + let state_weak = Arc::downgrade(&state); + gesture_click.connect_released(move |_, n_press, x, y| { + if n_press != 1 { + return; + } + + let state = state_weak.upgrade().unwrap(); + let mut state = state.lock().unwrap(); + state.release(x, y).unwrap(); + + let mut event: Event = Default::default(); + event.touch = (x as u16, y as u16); + send3(&event); + }); + event_controller.connect_motion(move |_, x, y| { + let mut state = state.lock().unwrap(); + state.motion(x, y).unwrap(); + + if state.is_pressed() { + let mut event: Event = Default::default(); + event.buttons = Buttons::TOUCH; + event.touch = (x as u16, y as u16); + send4(&event); + } + }); + drawing_area.add_controller(&gesture_click); + drawing_area.add_controller(&event_controller); + drawing_area.set_draw_func(move |_, ctx, _, _| { + ctx.set_source_rgb(1., 0., 0.); + ctx.set_operator(cairo::Operator::Screen); + ctx.paint().unwrap(); + }); + + hbox.append(&tools_box); + hbox.append(&drawing_area); + + let window = gtk::ApplicationWindow::builder() + .application(app) + .title("tablet-emu") + .default_width(800) + .default_height(480) + .child(&hbox) + .build(); + + window.show(); +} + +pub fn main() { + let app = gtk::Application::builder() + .application_id("fr.linkmauve.TabletEmu") + .build(); + app.connect_activate(build_ui); + app.run(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/bin/server.rs Thu Aug 26 22:15:55 2021 +0200 @@ -0,0 +1,102 @@ +// Tablet emulator, for people who don’t own one +// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use input_linux::Key; +use std::io::{self, ErrorKind}; +use std::net::UdpSocket; +use tablet_emu::protocol::{Buttons, Event}; +use tablet_emu::state::State; + +pub fn run_server(address: &str) -> io::Result<()> { + let mut state = match State::new() { + Ok(state) => state, + Err(err) => { + match err.kind() { + ErrorKind::NotFound => { + eprintln!("Couldn’t find /dev/uinput: {}", err); + eprintln!("Maybe you forgot to `modprobe uinput`?"); + } + ErrorKind::PermissionDenied => { + eprintln!("Couldn’t open /dev/uinput for writing: {}", err); + eprintln!("Maybe you aren’t allowed to create input devices?"); + } + _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err), + } + std::process::exit(1); + } + }; + + let socket = UdpSocket::bind(address)?; + println!("Listening on {:?}", socket); + println!("Here is an example client: https://hg.linkmauve.fr/remote-gamepad"); + + let mut event: Event = Default::default(); + let mut last = Some((0., 0.)); + state.set_size(320, 240); + loop { + // TODO: Yolo-alignment. + let buf: &mut [u8; 16] = unsafe { std::mem::transmute(&mut event) }; + let (amount, source) = socket.recv_from(buf)?; + if amount != std::mem::size_of::<Event>() { + eprintln!("Invalid data length: {}", amount); + continue; + } + println!("{:?} from {:?}", event, source); + if event.buttons.contains(Buttons::A) { + state.select_tool(Key::ButtonToolPen); + } else if event.buttons.contains(Buttons::B) { + state.select_tool(Key::ButtonToolRubber); + } else if event.buttons.contains(Buttons::X) { + state.select_tool(Key::ButtonToolBrush); + } else if event.buttons.contains(Buttons::Y) { + state.select_tool(Key::ButtonToolPencil); + } else if event.buttons.contains(Buttons::SELECT) { + state.select_tool(Key::ButtonToolAirbrush); + } else if event.buttons.contains(Buttons::RESIZE) { + println!( + "set_size({}, {})", + event.touch.0 as i32, event.touch.1 as i32 + ); + state.set_size(event.touch.0 as i32, event.touch.1 as i32); + } + let (x, y) = event.touch; + if event.buttons.contains(Buttons::TOUCH) { + if let None = last { + state.press(x as f64, y as f64)?; + last = Some((x as f64, y as f64)); + continue; + } + } else { + if let Some((x, y)) = last { + state.release(x, y)?; + last = None; + continue; + } + } + state.motion(x as f64, y as f64)?; + last = Some((x as f64, y as f64)); + } +} + +pub fn main() { + let args: Vec<_> = std::env::args().collect(); + let address = if args.len() > 1 { + args[1].clone() + } else { + String::from("0.0.0.0:16150") + }; + run_server(&address).unwrap(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/bin/standalone.rs Thu Aug 26 22:15:55 2021 +0200 @@ -0,0 +1,179 @@ +// Tablet emulator, for people who don’t own one +// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use gio::prelude::*; +use glib::clone; +use gtk::prelude::*; +use input_linux::Key; +use std::io::ErrorKind; +use std::sync::{Arc, Mutex}; +use tablet_emu::{state::State, DEFAULT_HEIGHT, DEFAULT_WIDTH}; + +fn build_main_menu(app: >k::Application) { + let quit = gio::SimpleAction::new("quit", None); + app.set_accels_for_action("app.quit", &["<Control>q"]); + app.add_action(&quit); + quit.connect_activate(clone!(@weak app => move |_, _| app.quit())); + + let about = gio::SimpleAction::new("about", None); + app.add_action(&about); + about.connect_activate(|_, _| { + let about = gtk::AboutDialog::builder() + .program_name("TabletEmu") + .logo_icon_name("input-tablet") + .website("https://hg.linkmauve.fr/tablet-emu") + .version("0.1") + .license_type(gtk::License::Agpl30) + .copyright("© 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>") + .build(); + //about.run(); + about.destroy(); + }); + + let menu = gio::Menu::new(); + { + let file = gio::Menu::new(); + file.append(Some("_Quit"), Some("app.quit")); + menu.append_submenu(Some("_File"), &file); + } + { + let help = gio::Menu::new(); + help.append(Some("_About"), Some("app.about")); + menu.append_submenu(Some("_Help"), &help); + } + app.set_menubar(Some(&menu)); +} + +fn build_ui(app: >k::Application) { + build_main_menu(app); + + let state = match State::new() { + Ok(state) => Arc::new(Mutex::new(state)), + Err(err) => { + match err.kind() { + ErrorKind::NotFound => { + eprintln!("Couldn’t find /dev/uinput: {}", err); + eprintln!("Maybe you forgot to `modprobe uinput`?"); + } + ErrorKind::PermissionDenied => { + eprintln!("Couldn’t open /dev/uinput for writing: {}", err); + eprintln!("Maybe you aren’t allowed to create input devices?"); + } + _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err), + } + std::process::exit(1); + } + }; + + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + let tools_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + + macro_rules! impl_tool { + ($tool:tt) => { + let tool = gtk::Button::with_mnemonic($tool); + let state_weak = Arc::downgrade(&state); + tool.connect_clicked(move |b| { + let state = state_weak.upgrade().unwrap(); + let mut state = state.lock().unwrap(); + let tool = match b.label().unwrap().as_str() { + "_Pen" => Key::ButtonToolPen, + "_Rubber" => Key::ButtonToolRubber, + "_Brush" => Key::ButtonToolBrush, + "P_encil" => Key::ButtonToolPencil, + "_Airbrush" => Key::ButtonToolAirbrush, + _ => unreachable!(), + }; + state.select_tool(tool); + }); + tools_box.append(&tool); + }; + } + impl_tool!("_Pen"); + impl_tool!("_Rubber"); + impl_tool!("_Brush"); + impl_tool!("P_encil"); + impl_tool!("_Airbrush"); + + let drawing_area = gtk::DrawingArea::builder() + .content_width(DEFAULT_WIDTH) + .content_height(DEFAULT_HEIGHT) + .hexpand(true) + .build(); + let gesture_click = gtk::GestureClick::new(); + let event_controller = gtk::EventControllerMotion::new(); + let state_weak = Arc::downgrade(&state); + drawing_area.connect_resize(move |_, width, height| { + let state = state_weak.upgrade().unwrap(); + let mut state = state.lock().unwrap(); + state.set_size(width, height); + }); + let state_weak = Arc::downgrade(&state); + gesture_click.connect_pressed(move |_, n_press, x, y| { + if n_press != 1 { + return; + } + + let state = state_weak.upgrade().unwrap(); + let mut state = state.lock().unwrap(); + state.press(x, y).unwrap(); + }); + let state_weak = Arc::downgrade(&state); + gesture_click.connect_released(move |_, n_press, x, y| { + if n_press != 1 { + return; + } + + let state = state_weak.upgrade().unwrap(); + let mut state = state.lock().unwrap(); + state.release(x, y).unwrap(); + }); + event_controller.connect_motion(move |_, x, y| { + let mut state = state.lock().unwrap(); + state.motion(x, y).unwrap(); + }); + drawing_area.add_controller(&gesture_click); + drawing_area.add_controller(&event_controller); + drawing_area.set_draw_func(move |_, ctx, _, _| { + ctx.set_source_rgb(1., 0., 0.); + ctx.set_operator(cairo::Operator::Screen); + ctx.paint().unwrap(); + }); + + hbox.append(&tools_box); + hbox.append(&drawing_area); + + let window = gtk::ApplicationWindow::builder() + .application(app) + .title("tablet-emu") + .default_width(800) + .default_height(480) + .child(&hbox) + .build(); + + window.show(); +} + +pub fn main() { + let app = gtk::Application::builder() + .application_id("fr.linkmauve.TabletEmu") + .build(); + app.connect_activate(build_ui); + app.run(); +}
--- a/src/client.rs Thu Aug 26 22:00:55 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,213 +0,0 @@ -// Tablet emulator, for people who don’t own one -// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -use crate::{state::FakeState, DEFAULT_HEIGHT, DEFAULT_WIDTH}; -use gio::prelude::*; -use glib::clone; -use gtk::prelude::*; -use input_linux::Key; -use std::sync::{Arc, Mutex}; -use std::net::UdpSocket; -use crate::Event; -use crate::server::Buttons; - -fn build_main_menu(app: >k::Application) { - let quit = gio::SimpleAction::new("quit", None); - app.set_accels_for_action("app.quit", &["<Control>q"]); - app.add_action(&quit); - quit.connect_activate(clone!(@weak app => move |_, _| app.quit())); - - let about = gio::SimpleAction::new("about", None); - app.add_action(&about); - about.connect_activate(|_, _| { - let about = gtk::AboutDialog::builder() - .program_name("TabletEmu") - .logo_icon_name("input-tablet") - .website("https://hg.linkmauve.fr/tablet-emu") - .version("0.1") - .license_type(gtk::License::Agpl30) - .copyright("© 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>") - .build(); - //about.run(); - about.destroy(); - }); - - let menu = gio::Menu::new(); - { - let file = gio::Menu::new(); - file.append(Some("_Quit"), Some("app.quit")); - menu.append_submenu(Some("_File"), &file); - } - { - let help = gio::Menu::new(); - help.append(Some("_About"), Some("app.about")); - menu.append_submenu(Some("_Help"), &help); - } - app.set_menubar(Some(&menu)); -} - -fn build_ui(app: >k::Application) { - build_main_menu(app); - - let address = "127.0.0.1:16150"; - let socket = Arc::new(Mutex::new(UdpSocket::bind("127.0.0.1:4321").unwrap())); - println!("opened {:?}", socket); - - let send = move |event: &Event| { - // TODO: Yolo-alignment. - let buf: &[u8; std::mem::size_of::<Event>()] = unsafe { std::mem::transmute(event) }; - let socket = socket.lock().unwrap(); - let amount = socket.send_to(buf, address).unwrap(); - assert_eq!(amount, std::mem::size_of::<Event>()); - }; - let send2 = send.clone(); - let send3 = send.clone(); - let send4 = send.clone(); - - let state = Arc::new(Mutex::new(FakeState::new())); - - let hbox = gtk::Box::builder() - .orientation(gtk::Orientation::Horizontal) - .build(); - let tools_box = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - macro_rules! impl_tool { - ($tool:tt) => { - let tool = gtk::Button::with_mnemonic($tool); - let state_weak = Arc::downgrade(&state); - let send = send.clone(); - tool.connect_clicked(move |b| { - let state = state_weak.upgrade().unwrap(); - let mut state = state.lock().unwrap(); - let tool = match b.label().unwrap().as_str() { - "_Pen" => Key::ButtonToolPen, - "_Rubber" => Key::ButtonToolRubber, - "_Brush" => Key::ButtonToolBrush, - "P_encil" => Key::ButtonToolPencil, - "_Airbrush" => Key::ButtonToolAirbrush, - _ => unreachable!(), - }; - state.select_tool(tool); - - let mut event: Event = Default::default(); - event.buttons = match tool { - Key::ButtonToolPen => Buttons::A, - Key::ButtonToolRubber => Buttons::B, - Key::ButtonToolBrush => Buttons::X, - Key::ButtonToolPencil => Buttons::Y, - Key::ButtonToolAirbrush => Buttons::SELECT, - _ => unreachable!(), - }; - send(&event); - }); - tools_box.append(&tool); - }; - } - impl_tool!("_Pen"); - impl_tool!("_Rubber"); - impl_tool!("_Brush"); - impl_tool!("P_encil"); - impl_tool!("_Airbrush"); - - let drawing_area = gtk::DrawingArea::builder() - .content_width(DEFAULT_WIDTH) - .content_height(DEFAULT_HEIGHT) - .hexpand(true) - .build(); - let gesture_click = gtk::GestureClick::new(); - let event_controller = gtk::EventControllerMotion::new(); - let state_weak = Arc::downgrade(&state); - drawing_area.connect_resize(move |_, width, height| { - let state = state_weak.upgrade().unwrap(); - let mut state = state.lock().unwrap(); - state.set_size(width, height); - - let mut event: Event = Default::default(); - event.buttons = Buttons::RESIZE; - event.touch = (width as u16, height as u16); - send(&event); - }); - let state_weak = Arc::downgrade(&state); - gesture_click.connect_pressed(move |_, n_press, x, y| { - if n_press != 1 { - return; - } - - let state = state_weak.upgrade().unwrap(); - let mut state = state.lock().unwrap(); - state.press(x, y).unwrap(); - - let mut event: Event = Default::default(); - event.buttons = Buttons::TOUCH; - event.touch = (x as u16, y as u16); - send2(&event); - }); - let state_weak = Arc::downgrade(&state); - gesture_click.connect_released(move |_, n_press, x, y| { - if n_press != 1 { - return; - } - - let state = state_weak.upgrade().unwrap(); - let mut state = state.lock().unwrap(); - state.release(x, y).unwrap(); - - let mut event: Event = Default::default(); - event.touch = (x as u16, y as u16); - send3(&event); - }); - event_controller.connect_motion(move |_, x, y| { - let mut state = state.lock().unwrap(); - state.motion(x, y).unwrap(); - - if state.is_pressed() { - let mut event: Event = Default::default(); - event.buttons = Buttons::TOUCH; - event.touch = (x as u16, y as u16); - send4(&event); - } - }); - drawing_area.add_controller(&gesture_click); - drawing_area.add_controller(&event_controller); - drawing_area.set_draw_func(move |_, ctx, _, _| { - ctx.set_source_rgb(1., 0., 0.); - ctx.set_operator(cairo::Operator::Screen); - ctx.paint().unwrap(); - }); - - hbox.append(&tools_box); - hbox.append(&drawing_area); - - let window = gtk::ApplicationWindow::builder() - .application(app) - .title("tablet-emu") - .default_width(800) - .default_height(480) - .child(&hbox) - .build(); - - window.show(); -} - -pub fn main(_args: &[String]) { - let app = gtk::Application::builder() - .application_id("fr.linkmauve.TabletEmu") - .build(); - app.connect_activate(build_ui); - app.run(); -}
--- a/src/gtk.rs Thu Aug 26 22:00:55 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,179 +0,0 @@ -// Tablet emulator, for people who don’t own one -// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -use crate::{state::State, DEFAULT_HEIGHT, DEFAULT_WIDTH}; -use gio::prelude::*; -use glib::clone; -use gtk::prelude::*; -use input_linux::Key; -use std::io::ErrorKind; -use std::sync::{Arc, Mutex}; - -fn build_main_menu(app: >k::Application) { - let quit = gio::SimpleAction::new("quit", None); - app.set_accels_for_action("app.quit", &["<Control>q"]); - app.add_action(&quit); - quit.connect_activate(clone!(@weak app => move |_, _| app.quit())); - - let about = gio::SimpleAction::new("about", None); - app.add_action(&about); - about.connect_activate(|_, _| { - let about = gtk::AboutDialog::builder() - .program_name("TabletEmu") - .logo_icon_name("input-tablet") - .website("https://hg.linkmauve.fr/tablet-emu") - .version("0.1") - .license_type(gtk::License::Agpl30) - .copyright("© 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>") - .build(); - //about.run(); - about.destroy(); - }); - - let menu = gio::Menu::new(); - { - let file = gio::Menu::new(); - file.append(Some("_Quit"), Some("app.quit")); - menu.append_submenu(Some("_File"), &file); - } - { - let help = gio::Menu::new(); - help.append(Some("_About"), Some("app.about")); - menu.append_submenu(Some("_Help"), &help); - } - app.set_menubar(Some(&menu)); -} - -fn build_ui(app: >k::Application) { - build_main_menu(app); - - let state = match State::new() { - Ok(state) => Arc::new(Mutex::new(state)), - Err(err) => { - match err.kind() { - ErrorKind::NotFound => { - eprintln!("Couldn’t find /dev/uinput: {}", err); - eprintln!("Maybe you forgot to `modprobe uinput`?"); - } - ErrorKind::PermissionDenied => { - eprintln!("Couldn’t open /dev/uinput for writing: {}", err); - eprintln!("Maybe you aren’t allowed to create input devices?"); - } - _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err), - } - std::process::exit(1); - } - }; - - let hbox = gtk::Box::builder() - .orientation(gtk::Orientation::Horizontal) - .build(); - let tools_box = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - macro_rules! impl_tool { - ($tool:tt) => { - let tool = gtk::Button::with_mnemonic($tool); - let state_weak = Arc::downgrade(&state); - tool.connect_clicked(move |b| { - let state = state_weak.upgrade().unwrap(); - let mut state = state.lock().unwrap(); - let tool = match b.label().unwrap().as_str() { - "_Pen" => Key::ButtonToolPen, - "_Rubber" => Key::ButtonToolRubber, - "_Brush" => Key::ButtonToolBrush, - "P_encil" => Key::ButtonToolPencil, - "_Airbrush" => Key::ButtonToolAirbrush, - _ => unreachable!(), - }; - state.select_tool(tool); - }); - tools_box.append(&tool); - }; - } - impl_tool!("_Pen"); - impl_tool!("_Rubber"); - impl_tool!("_Brush"); - impl_tool!("P_encil"); - impl_tool!("_Airbrush"); - - let drawing_area = gtk::DrawingArea::builder() - .content_width(DEFAULT_WIDTH) - .content_height(DEFAULT_HEIGHT) - .hexpand(true) - .build(); - let gesture_click = gtk::GestureClick::new(); - let event_controller = gtk::EventControllerMotion::new(); - let state_weak = Arc::downgrade(&state); - drawing_area.connect_resize(move |_, width, height| { - let state = state_weak.upgrade().unwrap(); - let mut state = state.lock().unwrap(); - state.set_size(width, height); - }); - let state_weak = Arc::downgrade(&state); - gesture_click.connect_pressed(move |_, n_press, x, y| { - if n_press != 1 { - return; - } - - let state = state_weak.upgrade().unwrap(); - let mut state = state.lock().unwrap(); - state.press(x, y).unwrap(); - }); - let state_weak = Arc::downgrade(&state); - gesture_click.connect_released(move |_, n_press, x, y| { - if n_press != 1 { - return; - } - - let state = state_weak.upgrade().unwrap(); - let mut state = state.lock().unwrap(); - state.release(x, y).unwrap(); - }); - event_controller.connect_motion(move |_, x, y| { - let mut state = state.lock().unwrap(); - state.motion(x, y).unwrap(); - }); - drawing_area.add_controller(&gesture_click); - drawing_area.add_controller(&event_controller); - drawing_area.set_draw_func(move |_, ctx, _, _| { - ctx.set_source_rgb(1., 0., 0.); - ctx.set_operator(cairo::Operator::Screen); - ctx.paint().unwrap(); - }); - - hbox.append(&tools_box); - hbox.append(&drawing_area); - - let window = gtk::ApplicationWindow::builder() - .application(app) - .title("tablet-emu") - .default_width(800) - .default_height(480) - .child(&hbox) - .build(); - - window.show(); -} - -pub fn main(_args: &[String]) { - let app = gtk::Application::builder() - .application_id("fr.linkmauve.TabletEmu") - .build(); - app.connect_activate(build_ui); - app.run(); -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib.rs Thu Aug 26 22:15:55 2021 +0200 @@ -0,0 +1,25 @@ +// Tablet emulator, for people who don’t own one +// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +pub mod protocol; +pub mod state; +pub mod uinput; + +pub const MAX_X: i32 = 69920; +pub const MAX_Y: i32 = 39980; + +pub const DEFAULT_WIDTH: i32 = 320; +pub const DEFAULT_HEIGHT: i32 = 180;
--- a/src/main.rs Thu Aug 26 22:00:55 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,70 +0,0 @@ -// Tablet emulator, for people who don’t own one -// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -#[cfg(feature = "gui")] -mod gtk; -#[cfg(feature = "gui")] -mod client; -mod server; -mod state; -mod uinput; - -pub use server::Event; - -use std::env::args; - -const MAX_X: i32 = 69920; -const MAX_Y: i32 = 39980; - -const DEFAULT_WIDTH: i32 = 320; -const DEFAULT_HEIGHT: i32 = 180; - -#[derive(Debug)] -enum Ui { - Gtk, - Server, - Client, -} - -fn main() { - let mut args: Vec<_> = args().collect(); - let ui = match if args.len() > 1 { - args.remove(1) - } else { - String::from("client") - } - .as_str() - { - "gui" => Ui::Gtk, - "server" => Ui::Server, - "client" => Ui::Client, - name => { - eprintln!("Wrong UI “{}”, expected gui or server.", name); - std::process::exit(2); - } - }; - - match ui { - #[cfg(feature = "gui")] - Ui::Gtk => gtk::main(&args), - - #[cfg(not(feature = "gui"))] - Ui::Gtk => panic!("tablet-emu has been compiled without GUI support."), - - Ui::Server => server::main(&args), - Ui::Client => client::main(&args), - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/protocol.rs Thu Aug 26 22:15:55 2021 +0200 @@ -0,0 +1,64 @@ +// Tablet emulator, for people who don’t own one +// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use bitflags::bitflags; + +bitflags! { + /// This is the memory layout of the buttons on the 3DS. + #[derive(Default)] + pub struct Buttons: u32 { + const A = 0x00000001; + const B = 0x00000002; + const SELECT = 0x00000004; + const START = 0x00000008; + const RIGHT = 0x00000010; + const LEFT = 0x00000020; + const UP = 0x00000040; + const DOWN = 0x00000080; + const R = 0x00000100; + const L = 0x00000200; + const X = 0x00000400; + const Y = 0x00000800; + // Nothing + // Nothing + const ZL = 0x00004000; + const ZR = 0x00008000; + const RESIZE = 0x00010000; // Not an actual 3DS button! + // Nothing + // Nothing + // Nothing + const TOUCH = 0x00100000; + // Nothing + // Nothing + // Nothing + const C_RIGHT = 0x01000000; + const C_LEFT = 0x02000000; + const C_UP = 0x04000000; + const C_DOWN = 0x08000000; + const CIRCLE_RIGHT = 0x10000000; + const CIRCLE_LEFT = 0x20000000; + const CIRCLE_UP = 0x40000000; + const CIRCLE_DOWN = 0x80000000; + } +} + +#[derive(Debug, Default)] +pub struct Event { + pub buttons: Buttons, + pad: (i16, i16), + c_pad: (i16, i16), + pub touch: (u16, u16), +}
--- a/src/server.rs Thu Aug 26 22:00:55 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,145 +0,0 @@ -// Tablet emulator, for people who don’t own one -// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -use crate::state::State; -use bitflags::bitflags; -use input_linux::Key; -use std::io::{self, ErrorKind}; -use std::net::UdpSocket; - -bitflags! { - /// This is the memory layout of the buttons on the 3DS. - #[derive(Default)] - pub struct Buttons: u32 { - const A = 0x00000001; - const B = 0x00000002; - const SELECT = 0x00000004; - const START = 0x00000008; - const RIGHT = 0x00000010; - const LEFT = 0x00000020; - const UP = 0x00000040; - const DOWN = 0x00000080; - const R = 0x00000100; - const L = 0x00000200; - const X = 0x00000400; - const Y = 0x00000800; - // Nothing - // Nothing - const ZL = 0x00004000; - const ZR = 0x00008000; - const RESIZE = 0x00010000; // Not an actual 3DS button! - // Nothing - // Nothing - // Nothing - const TOUCH = 0x00100000; - // Nothing - // Nothing - // Nothing - const C_RIGHT = 0x01000000; - const C_LEFT = 0x02000000; - const C_UP = 0x04000000; - const C_DOWN = 0x08000000; - const CIRCLE_RIGHT = 0x10000000; - const CIRCLE_LEFT = 0x20000000; - const CIRCLE_UP = 0x40000000; - const CIRCLE_DOWN = 0x80000000; - } -} - -#[derive(Debug, Default)] -pub struct Event { - pub buttons: Buttons, - pad: (i16, i16), - c_pad: (i16, i16), - pub touch: (u16, u16), -} - -pub fn run_server(address: &str) -> io::Result<()> { - let mut state = match State::new() { - Ok(state) => state, - Err(err) => { - match err.kind() { - ErrorKind::NotFound => { - eprintln!("Couldn’t find /dev/uinput: {}", err); - eprintln!("Maybe you forgot to `modprobe uinput`?"); - } - ErrorKind::PermissionDenied => { - eprintln!("Couldn’t open /dev/uinput for writing: {}", err); - eprintln!("Maybe you aren’t allowed to create input devices?"); - } - _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err), - } - std::process::exit(1); - } - }; - - let socket = UdpSocket::bind(address)?; - println!("Listening on {:?}", socket); - println!("Here is an example client: https://hg.linkmauve.fr/remote-gamepad"); - - let mut event: Event = Default::default(); - let mut last = Some((0., 0.)); - state.set_size(320, 240); - loop { - // TODO: Yolo-alignment. - let buf: &mut [u8; 16] = unsafe { std::mem::transmute(&mut event) }; - let (amount, source) = socket.recv_from(buf)?; - if amount != std::mem::size_of::<Event>() { - eprintln!("Invalid data length: {}", amount); - continue; - } - println!("{:?} from {:?}", event, source); - if event.buttons.contains(Buttons::A) { - state.select_tool(Key::ButtonToolPen); - } else if event.buttons.contains(Buttons::B) { - state.select_tool(Key::ButtonToolRubber); - } else if event.buttons.contains(Buttons::X) { - state.select_tool(Key::ButtonToolBrush); - } else if event.buttons.contains(Buttons::Y) { - state.select_tool(Key::ButtonToolPencil); - } else if event.buttons.contains(Buttons::SELECT) { - state.select_tool(Key::ButtonToolAirbrush); - } else if event.buttons.contains(Buttons::RESIZE) { - println!("set_size({}, {})", event.touch.0 as i32, event.touch.1 as i32); - state.set_size(event.touch.0 as i32, event.touch.1 as i32); - } - let (x, y) = event.touch; - if event.buttons.contains(Buttons::TOUCH) { - if let None = last { - state.press(x as f64, y as f64)?; - last = Some((x as f64, y as f64)); - continue; - } - } else { - if let Some((x, y)) = last { - state.release(x, y)?; - last = None; - continue; - } - } - state.motion(x as f64, y as f64)?; - last = Some((x as f64, y as f64)); - } -} - -pub fn main(args: &[String]) { - let address = if args.len() > 1 { - args[1].clone() - } else { - String::from("0.0.0.0:16150") - }; - run_server(&address).unwrap(); -}
