From 89052221c847f8f4e24ae1f5bd37759a60d0e173 Mon Sep 17 00:00:00 2001 From: cos Date: Wed, 5 Jun 2024 19:46:15 +0200 Subject: demo: Proof-of-concept, dual-thread design --- .gitignore | 2 + Cargo.toml | 8 +++ src/main.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b72444 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/Cargo.lock +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..863b742 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ccc" +version = "0.0.1" +edition = "2021" + +[dependencies] +crossbeam-channel = "0.5.13" +cursive = "0.20.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..13d24d6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,170 @@ +use { + crossbeam_channel::{ + Receiver, + Sender, + + unbounded, + }, + cursive::{ + Cursive, + CursiveExt, + Printer, + Rect, + View, + XY, + + direction::Direction, + event::{ + Event, + EventResult, + }, + view::{ + CannotFocus, + Nameable, + Selector, + ViewNotFound, + }, + views::{ + Button, + LinearLayout, + TextView, + } + }, + std::{ + time::Duration, + thread::{ + self, + sleep, + }, + } +}; + +type CursiveSender = Sender FnOnce(&'a mut Cursive) + Send + 'static)>>; + +struct DataView { + data: Option, + view: TextView, +} + +impl DataView { + pub fn new() -> Self { + Self { + data: None, + view: TextView::new("uninitialized"), + } + } + + fn set_data(&mut self, data: &str) { + self.data = Some(data.to_owned()); + self.view.set_content(data.to_string()) + } +} + +impl View for DataView { + fn draw(&self, printer: &Printer<'_, '_>) { + self.view.draw(printer) + } + + fn layout(&mut self, xy: XY) { + self.view.layout(xy) + } + + fn needs_relayout(&self) -> bool { + self.view.needs_relayout() + } + + fn required_size(&mut self, constraint: XY) -> XY { + self.view.required_size(constraint) + } + + fn on_event(&mut self, event: Event) -> EventResult { + self.view.on_event(event) + } + + // fn call_on_any(), should not be needed for this view. + + fn focus_view(&mut self, selector: &Selector<'_>) -> Result { + self.view.focus_view(selector) + } + + fn take_focus( &mut self, source: Direction) -> Result { + self.view.take_focus(source) + } + + fn important_area(&self, view_size: XY) -> Rect { + self.view.important_area(view_size) + } + + fn type_name(&self) -> &'static str { + "DataView" + } +} + +fn data_request(cur: &mut Cursive) { + if let Some(sender) = cur.user_data::>() { + let _ = sender.send(true); + } +} + +fn populate_data(s: &mut Cursive, is_data: bool) { + if let Some(mut data_view) = s.find_name::("populate_me") { + if is_data { + data_view.set_data("populated") + } else { + data_view.set_data("initialized") + } + } +} + +fn background_thread(reciever: Receiver, sender: CursiveSender) { + while let Ok(msg) = reciever.recv() { + eprintln!("Received a message. Sleeping 3 seconds before sending response."); + sleep(Duration::from_secs(3)); + let _ = sender.send(Box::new(move |s| populate_data(s, msg))); + } +} + +fn main() { + // In order to maintain the main thread responsive in dealing with the user interface, we wish + // to offload all server communication to a background thread. This code demonstrates how to + // send a message from the ui, process it in the background and send the result back for the + // user interface to update the content of its view. + + // Firstly, we need a channel pair for sending a message from the ui to the worker thread. + let (wrk_sender, wrk_receiver) = unbounded::(); + + // We then construct the actual user interface. Our DataView, a button to trigger populating + // it and a button to quit the app. + let mut app = Cursive::new(); + let mut horizontal = LinearLayout::horizontal(); + let mut vertical = LinearLayout::vertical(); + let data_view = DataView::new().with_name("populate_me"); + let populate_button = Button::new("data", data_request); + let quit_button = Button::new("quit", |s| s.quit()); + vertical.add_child(data_view); + horizontal.add_child(populate_button); + horizontal.add_child(quit_button); + vertical.add_child(horizontal); + app.add_layer(vertical); + + // Once having created the app, we send a request to initialize our view. + let _ = wrk_sender.send(false); + + // We then place the sender in the user_data, to make it available for any ui function. + app.set_user_data(wrk_sender); + + // Getting the callback sender from the user interface, needed for the worker to respond. + let ui_sender = app.cb_sink().clone(); + + // Then we launch the background worker thread, providing it the appropriate channel ends. + let _worker = thread::spawn(move || background_thread(wrk_receiver, ui_sender)); + + // Lastly we launch the user interface. + app.run(); + + // Three seconds after launching, the text changes from "uninitialized" to "initialized" and + // further, three seconds after pressing the button the text becomes "populated". + // + // Essentially, this design should work for the complete program. Yet with some slightly more + // expressive choice of datatypes than merely a bool. +} -- cgit v1.2.3