Here's another FLTK.rs question. I really should be able to do this, but it just isn't working. I had the bright idea to use a TextEditor widget to display the title of a data display window. The idea is that then the Title will be user editable, thus making the user experience more flexible. However, whether the user edits the title window or the title is changed elsewhere in the program, I need to be able to refresh the text in that widget, and that is where I'm getting frustrated. The code below generates a parent window and adds a TextEditor box to it. That displays just fine. I then use the function change_text() to create some new text for the window. The issue is that if I call the change_text() function either before or after the win.end() -- win.show() sequence the window will not show until after the change_text() function has completed, so the initial text is never shown. I've played around with different placements of the change_text() function, with no success. Here's the code:
fn main() {
let app = app::App::default();
let mut win = Window::new(300, 100, 700, 800, "Hello from Rust");
let mut buf = TextBuffer::default();
buf.set_text("Initial Text Initial Text");
let buf_clone = buf.clone();
let mut ted = TextEditor::new(50, 50, win.w() - 75, 200, "");
ted.set_buffer(buf.clone());
ted.set_color(Color::Green);
win.end();
win.show();
change_text(&mut ted, &mut buf);
app.run().unwrap();
}
fn change_text(ted: &mut TextEditor, buf: &mut TextBuffer) {
util_wait_for_enter();
buf.set_text("New Text");
let buf_clone = buf.clone();
ted.set_buffer(buf_clone);
ted.redraw();
}
I suspect that the issue is that I'm not really understanding asynchronous nature of an FLTK app::App. Still, I need some help so comments will be appreciated.
FYI, the util_wait_for_enter()' function does just that. It waits until Enter` is pressed and then continues to execute the program. I'm thinking you might need the code for that function, so here it is:
fn util_wait_for_enter() {
println!("Press 'Enter' to continue... \n");
io::stdout().flush().unwrap(); // Flush stdout to ensure the message is displayed immediately
// Read a single byte from stdin
let mut buffer = [0u8; 1];
io::stdin().read_exact(&mut buffer).unwrap();
}
I'm not sure I understand the question.
But running the program, the editor's content is "New text", and that's what the change_text() function is doing.
Note that the window is never actually shown until the event loop runs (App::run). So an io blocking function will block until it returns. If you want the window to show before running the blocking function, you will need to use a callback of some sorts. Although fltk-rs offers App::wait() which allows you to inject code into the event loop, you don't really want to block there.
Sorry about being unclear. Here's the sequence I'm looking for:
The window shows with the text "Initial Text" showing in the TextEditor widget.
It pauses so that I can see that the "Initial Text" is indeed showing correctly.
When Enter is pressed, the window refreshes and "New Text" is shown in the widget.
I put that pause in there to help me with debugging. I wanted to make sure that the window was showing with the "Initial Text" in the widget. The idea is that the Title text can be changed by some other function with the new text replacing the old.
In swyh-rs I use a loop with app::wait() that never blocks but gets activated by app::awake() as needed by other threads or callbacks that use crossbeam channels to send messages to the main thread.
The app:await() loop then checks these channels with try_recv() to do what is needed.
I'm far from an fltk-rs expert (I even hate GUI programming) but it works.
Before your app.run().unwrap();. This should show the window with its initial text, then when hitting enter in the terminal, it changes the text of the buffer.
Well, that worked and, after I changed the time parameter to 10.0, it lets me see both the initial text and the new text displayed as wanted. Thanks. Next is to incorporate this into my project. It's not working yet, so I suspect I'll be back.
Thanks for your help!
FYI, here's the code that worked:
se fltk::app;
use fltk::enums::Color;
use fltk::prelude::{DisplayExt, GroupExt, WidgetBase, WidgetExt};
use fltk::text::{TextBuffer, TextEditor};
use fltk::window::Window;
fn main() {
let app = app::App::default();
let mut win = Window::new(300, 100, 700, 800, "Hello from Rust");
let mut buf = TextBuffer::default();
buf.set_text("Initial Text Initial Text");
let buf_clone = buf.clone();
let mut ted = TextEditor::new(50, 50, win.w() - 75, 200, "");
ted.set_buffer(buf.clone());
ted.set_color(Color::Green);
win.end();
win.show();
app::add_timeout3(10.0, move |h| {
change_text(&mut ted);
});
app.run().unwrap();
}
fn change_text(ted: &mut TextEditor) {
let mut buf = TextBuffer::default();
buf.set_text("NEW TEXT NEW TEXT NEW TEXT");
let buf_clone = buf.clone();
ted.set_buffer(buf_clone);
}
So, in order to change the display in an FLTK TextEditor, all I really need are these three lines of code:
let mut buf = TextBuffer::default();
buf.set_text("NEW TEXT NEW TEXT NEW TEXT");
text_editor.set_buffer(buf);
and it is working fine here in the code we've been discussing. However, it isn't working when I try it in my main project. In my application the TextEditor widget is saved as a field in a struct (called Wdgts) and the new text comes from data stored in a different struct (called Banks). Both of those structs are global. That second struct is first filled after reading the data (using serde.json) from a file and then I call the function below in an attempt to use the newly loaded data to refresh the TextEditor widget that I'm using to display my title. I'm telling you all this so you can get some idea of what the code is doing without me copying it all here. Here is some of it, though, so that you can maybe catch my mistake that is preventing the content of the TextEditor box from refreshing:
pub fn bnk_refresh_title() {
let usebank: Bank;
let mut wdgts: Wdgts;
{
usebank = CURRENT_BANK.lock().unwrap().clone();
wdgts = WIDGETS.lock().unwrap().clone();
} // Access global structs.
let mut buf = TextBuffer::default();
buf.set_text(usebank.bank_title.as_str()); // Uses the title field from the current Bank struct.
wdgts.title_editbox.set_buffer(buf); // Successfully sets the content of the `TextEditor` widget to the new text now contained in `buf`.
let title_text = wdgts.title_editbox.buffer().unwrap().text();
println!("\n Waypoint 4. The title edit box should now contain: \n {:?} \n", title_text);
}
The last two lines are there for debugging purposes and tell me that the new text that I want to display in the TextEditor widget is correct and is indeed stored in the buffer just as it should be. But that is not what is showing on screen. And......it really should be showing. So.....help?
Is bnk_refresh_title being called from a different thread/execution context than the main ui? If so you can try calling app::awake() to notify the gui of the change. If not, I’m not sure what the problem is. If yoyr repo is public, you can post it here or privately, or try to create a minimal reproducible example which demonstrates the issue.
Well, that's what I was trying to do here, except that with this code (once we got that timeout thing working) it always worked fine. I tried add app::awake() to my code, but nothing changed. I'll keep fussing with it.
I haven't published this on github yet. Don't know how, really. I've been putting off doing that until I have a working prototype that will help me explain to others what this piece of software is all about. Then, too, I'm pretty sure my amateur lack of ability will just show through. My code is really kind of messy and, well........ embarassing? For instance, I'm really, really bad at error handling. It started out as a terminal-based app and I don't yet have it all changed over to the FLTK gui. That makes the interface really kind of weird, especially if you aren't expecting it. Bouncing back and forth between the gui and the terminal is odd at best.
Anyway, I'm going to keep fussing with this. I really need to be able to load a Bank struct from a file and then refresh all the widgets with that data. Getting the title to refresh properly will be a good start.
I agree with @parasyte , it would be interesting to know how you initialize your global objects and modify them. fltk's widgets don't have const constructors so they don't lend themselves nicely to some global Mutex<SomeWidget>. I've tried to fill some blanks here, although I don't recommend initializing widgets that way:
use fltk::{prelude::*, *};
use std::sync::{Mutex, LazyLock};
#[derive(Default, Clone)]
struct Bank {
bank_title: String,
}
#[derive(Default, Clone)]
struct Wdgts {
title_editbox: text::TextEditor,
}
static CURRENT_BANK: Mutex<Bank> = Mutex::new(Bank { bank_title: String::new() });
static WIDGETS: LazyLock<Mutex<Wdgts>> = LazyLock::new(|| {
let mut b = text::TextBuffer::default();
b.set_text("Initial text");
let mut title_editbox = text::TextEditor::new(0, 0, 0, 0, "");
title_editbox.set_buffer(b);
Mutex::new(Wdgts {
title_editbox,
})
});
pub fn bnk_fetch_data() {
let mut usebank = CURRENT_BANK.lock().unwrap();
// fetch data from somewhere, assuming we've done that
usebank.bank_title = "Some bank".to_string();
}
pub fn bnk_refresh_title() {
let usebank: Bank;
let mut wdgts: Wdgts;
{
usebank = CURRENT_BANK.lock().unwrap().clone();
wdgts = WIDGETS.lock().unwrap().clone();
} // Access global structs.
let mut buf = text::TextBuffer::default();
buf.set_text(usebank.bank_title.as_str()); // Uses the title field from the current Bank struct.
wdgts.title_editbox.set_buffer(buf); // Successfully sets the content of the `TextEditor` widget to the new text now contained in `buf`.
let title_text = wdgts.title_editbox.buffer().unwrap().text();
println!("\n Waypoint 4. The title edit box should now contain: \n {:?} \n", title_text);
}
fn main() {
let app = app::App::default();
let mut win = window::Window::default().with_size(600, 400);
let mut col = group::Flex::default_fill().column();
col.add(&WIDGETS.lock().unwrap().title_editbox);
let mut btn1 = button::Button::default().with_label("Fetch Bank Data");
col.fixed(&btn1, 30);
btn1.set_callback(|_| bnk_fetch_data());
let mut btn2 = button::Button::default().with_label("Refresh Editor Content");
col.fixed(&btn2, 30);
btn2.set_callback(|_| bnk_refresh_title());
col.end();
win.end();
win.show();
app.run().unwrap();
}
Yes, it does. I'll dig in and see what changes I can make.
Sorry I was a few days getting back to this. I spent them learning how to upload my project to Github. It's there now, although I still have to deal with some of my personal crates/libraries to which anyone trying to compile my code will need access. Here's a link:
You might have something there. Here's how I move the values from the global variables into the local variables:
let usebank: Bank;
let mut wdgts: Wdgts;
{
usebank = CURRENT_BANK.lock().unwrap().clone();
wdgts = WIDGETS.lock().unwrap().clone();
} // Load the global structs.
The global variables go in and out of scope quickly so that the locks will be released. And...... I am indeed using a clone. Would this cause me to be using/modifying the wrong thing? The values in the wdgts variable should be the same. I guess the question is whether the title_editbox widget is still the same widget I need. I wonder if I could get a way with dropping the clone() when I access those variables? It might work since the globals go out of scope so quickly. I'll have to give it a try.
One option is to use id's for your widgets, and not rely on a global WIDGETS.
So when instantiating your editor in make_title_txtedtr: let mut ted = TextEditor::new(0, 40, primwin.width(), 60, "").with_id("ted");
Then when you need to access it elsewhere: let mut ted: TextEditor = app::widget_from_id("ted").unwrap();
Similar for your Scroll. Since your qstn_boxes are children of your Scroll, you can iterate them from the Scroll, and you won't have to keep them in a Vec.
This will clearly work, as far as functioning correctly. Crucially, you don't do the same final update in the make_title_txtedtr() function. The modified clone is just thrown away! Fixing it is easy: Just copy-pasta that WIDGETS assignment to the last line in make_title_txtedtr().
On the other hand, I would feel bad if I didn't try to encourage you to change how state is managed in this application. Global variables are error-prone, as you can see from this issue you encountered.
And while cloning can make Rust "easy mode", it comes at the expense of introducing more errors that are very subtle and hard to detect. Additionally, the clone in that last assignment line doesn't do anything of value for you. It just clutters the code.
That said, I haven't completely reviewed the code. I cannot rule out that the global variables could be a good tradeoff of some kind. There is quite a bit of code in the project, and there may be some necessary complexity that I am unaware of.
Also... I am unfamiliar with FLTK. And though the clones appear to just be ref-counted, I'd have to delegate to @MoAlyousef's expertise, here. That assignment to the global variable may not do anything after all?
Well, it's going to take some time for me to dig into your comments. Thanks for digging in so deeply yourselves.
I tried to stay away from using global variables. Tried using ref-cells instead. The problem was that I was passing a variable to a function just so I could pass it on to another function that needed to pass it on one more time. I was playing 'kick the can' written in Rust and it added a massive amount of complexity (and potential errors) to the code. The global variables, finicky as they are, solved that problem. The error prone nature of globals probably isn't any worse than what I had before. You and @MoAlyousef have given me enough help now that I think I can now work through this issue. Thanks!
However as you've said, handling global state is quite challenging, especially when dealing with scopes, clones and locks.
@preacherdad That said, there are crates which should help with state management in fltk-rs, there's a discussion in the repo which mentions external crates, as well as an using an example using an observer pattern without any external crates