Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file system watcher to FileSelector and FileBrowser widgets #76

Closed
mrDIMAS opened this issue Mar 12, 2021 · 37 comments
Closed

Add file system watcher to FileSelector and FileBrowser widgets #76

mrDIMAS opened this issue Mar 12, 2021 · 37 comments
Labels
enhancement New feature or request user-interface

Comments

@mrDIMAS
Copy link
Member

mrDIMAS commented Mar 12, 2021

Currently, if you open a FileSelector or FileBrowser and navigate to a folder and then add some content to the folder, it won't show up until you collapse\expand the folder. It can be fixed by adding file system watcher which will notify about changes in file system.

@mrDIMAS mrDIMAS added enhancement New feature or request user-interface labels Mar 25, 2021
@gbutler69
Copy link
Contributor

I took a look at the code for the "file_browser" widget and it seems like there is going to need to be some integration of the "notify-rs" crate into the overall event loop and then send messages to the widget. Does that sound about right? Where should I be looking to do the event loop integration?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 15, 2021

I checked notify-rs's example and it should be no problem to "glue" it to the rg3d-ui.

This is just modified example from notify-rs docs:

use notify::{Watcher, RecommendedWatcher, RecursiveMode, Result};

fn setup_watcher<M: MessageData, C: Control<M, C>>(sender: Sender<UiMessage<M, C>>, path: &Path) -> Result<()> {
    let mut watcher = notify::recommended_watcher(|res| {
        match res {
           Ok(event) = { sender.send( specific_file_browser_message )... },
           Err(e) => println!("watch error: {:?}", e),
        }
    })?;

    // Add a path to be watched. All files and directories at that path and
    // below will be monitored for changes.
    watcher.watch(path, RecursiveMode::Recursive)?;

    Ok(())
}

It is very simple - rg3d-ui uses channels for communication, it allows us to send message from any thread (file system watcher works in separate thread). To get sender instance, simply call ui.sender() in the FileBrowserBuilder::build

@gbutler69
Copy link
Contributor

OK, I'm going to try implementing this and then creating a PR. I'm fairly beginner with Rust and no experience with RG3D so it might take me some time, but I'm pretty sure I can implement this.

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 17, 2021

Thanks! Just tell me if you need help, I'll do my best to help you.

@gbutler69
Copy link
Contributor

OK, I've made some progress, but I'm running into issues because "M" is not "Send" in the generics. I've added the following code to FileBrowserBuilder.build:

       let widget = self.widget_builder.with_child(grid).build();
        let ui_sender = ctx.ui.sender();
        let ui_to_send_to = widget.handle();
        let (tx, rx) = std::sync::mpsc::channel();
        let watcher = match notify::watcher(tx, time::Duration::from_secs(1)) {
            Ok(watcher) => Some(watcher),
            Err(_) => None,
        };
        let watcher_conversion_thread = std::thread::spawn(move || loop { // HERE GETTING ERROR THAT 'M' IS NOT Send - I'm kinda stuck here
            match rx.recv() {
                Ok(event) => match event {
                    notify::DebouncedEvent::NoticeRemove(path)
                    | notify::DebouncedEvent::Remove(path) => {
                        ui_sender.send(FileBrowserMessage::remove(
                            ui_to_send_to,
                            MessageDirection::ToWidget,
                            path,
                        ));
                    }
                    notify::DebouncedEvent::Create(path) => {
                        ui_sender.send(FileBrowserMessage::add(
                            ui_to_send_to,
                            MessageDirection::ToWidget,
                            path,
                        ));
                    }
                    notify::DebouncedEvent::Rescan | notify::DebouncedEvent::Error(_, _) => {
                        ui_sender.send(FileBrowserMessage::rescan(
                            ui_to_send_to,
                            MessageDirection::ToWidget,
                        ));
                    }
                    _ => (),
                },
                Err(_) => break,
            }
        });

        let browser = FileBrowser {
            widget,
            tree_root,
            path_text,
            path: match self.mode {
                FileBrowserMode::Open => self.path,
                FileBrowserMode::Save {
                    ref default_file_name,
                } => self.path.join(default_file_name),
            },
            file_name_value: match self.mode {
                FileBrowserMode::Open => Default::default(),
                FileBrowserMode::Save {
                    ref default_file_name,
                } => default_file_name.clone(),
            },
            filter: self.filter,
            mode: self.mode,
            scroll_viewer,
            root: self.root,
            file_name,
            watcher: Rc::new(watcher),
            watcher_conversion_thread,
        };

        ctx.add_node(UINode::FileBrowser(browser))

I'm not sure this approach is going to work based on the constraints of the types "M" involved. Any thoughts?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 20, 2021

You could just add Send trait bound to MessageData trait, which located in message.rs:1000, something like this:

pub trait MessageData: 'static + Debug + Clone + PartialEq + Send {}

I just checked and it compiles without any issues.

@gbutler69
Copy link
Contributor

Yeah, I tried that but then I get a host of other errors at the definition of the closure for the conversion thread. If I do that, I get the following:

std::rc::Rc<std::cell::RefCell<(dyn for<'r> std::ops::FnMut(&'r std::path::Path) -> bool + 'static)>>cannot be sent between threads safely withinmessage::UiMessage<M, C>, the trait std::marker::Sendis not implemented forstd::rc::Rc<std::cell::RefCell<(dyn for<'r> std::ops::FnMut(&'r std::path::Path) -> bool + 'static)>>required because of the requirements on the impl ofstd::marker::Sendforstd::sync::mpsc::Sender<message::UiMessage<M, C>>required because it appears within the type[closure@rg3d-ui/src/file_browser.rs:738:60: 766:10]rustcE0277 lib.rs(1, 1): required by a bound in this mod.rs(624, 8): required by this bound in std::thread::spawnfile_browser.rs(41, 12): required because it appears within the typefile_browser::Filterlib.rs(1, 1): required because it appears within the typestd::option::Option<file_browser::Filter>`

It seems like file_browser::Filter might be the issue now. I'll check if there is a way to make that Send as well. I'm not sure why that is an issue though. It doesn't seem like something that the closure is capturing.

@gbutler69
Copy link
Contributor

I believe the problem is here:

#[derive(Clone)]
pub struct Filter(pub Rc<RefCell<dyn FnMut(&Path) -> bool>>);

from file_browser.rs.

This seems like a problem in the Rc is not Send. What about making this an Arc? That would cascade into other things, but maybe it won't be too bad. What do you think?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 20, 2021

Ok, I think it is the time for unsafe. This is kinda annoying issue: the only place where C type is used is in PhantomData<C> somewhere inside Handle<UINode<M,C>> and this prevents compiler from correct Send impl propagation. So, remove #![forbid(unsafe_code)] at lib.rs and add unsafe impl<M: MessageData, C: Control<M, C>> Send for UiMessage<M, C> {} somewhere after struct UiMessage. UI in general is not multithreaded (that is why Control trait has no Sync or Send bound), but message passing is multithreaded, so by using unsafe (which is 100% safe here) we're telling the compiler that everything is fine.

@gbutler69
Copy link
Contributor

Do you mean to change Rc in Filter definition to an Arc<...> and also do this unsafe thing? Filter is currently not Send because of the Rc not due to C (Control). Are you seeing another compile issue that I haven't run into yet surrounding the "C"?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 20, 2021

No, just add unsafe thing. I'm pretty sure that without manual Send impl, it will be a footgun for someone else who'll decide to put Rc or other stuff like this in their own widgets.

@gbutler69
Copy link
Contributor

Leaving Filter be a wrapper around Rc will not be sound to Send. Even if it were to compile using unsafe, it would be unsound (that is, Undefined Behavior). I really think it needs to be an Arc, like:

      #[derive(Clone)]
pub struct Filter(pub Arc<Mutex<dyn FnMut(&Path) -> bool>>);

or somesuch. Otherwise, the "FileBrowserMessage" will not be "Send". If this is not "Send" then you can't send FileBrowserMessage(s) from the conversion thread to the UI thread.

Do you not agree?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 21, 2021

UiMessage does not contain any instance of any widget, so manual Send implementation is safe in this case. The only reason why it has C in generic arguments is to be able to contain handles to nodes (which are strongly typed).

@gbutler69
Copy link
Contributor

gbutler69 commented Aug 21, 2021

As near as I can tell, the issue is not "C". It is the Rc in Filter as well as teh FnMut not having the Send bound.
I did the following and it seems to resolve the issues without using unsafe:

file_browser.rs

pub struct Filter(pub Arc<Mutex<dyn FnMut(&Path) -> bool + Send>>);

and

message.rs

pub trait MessageData: 'static + Debug + Clone + PartialEq + Send {}

This seems sufficient to resolve the issues with sending UI Messages across thread boundaries.

EDIT: Of course, I updated the rest of the code to handle the change to Filter (which wasn't that many changes). If you're OK with it, I'll continue down this path.

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 21, 2021

Aha, that's good! Sorry, I was replying in the middle of the night and I was "a bit" tired 😅

@gbutler69
Copy link
Contributor

Hello again. I'm running into an issue that the messages are not getting through to the "handle_routed_message" method of the "FileBrowser". Here is the code that builds the FS watcher and conversion thread to send messages to the "FileBrowser":

In FileBrowserBuilder.build:

        let widget = self.widget_builder.with_child(grid).build();
        let widget_handle = widget.handle();
        let the_path = match &self.root {
            Some(path) => path.clone(),
            _ => self.path.clone(),
        };
        let browser = FileBrowser {
            widget,
            tree_root,
            path_text,
            path: match self.mode {
                FileBrowserMode::Open => self.path,
                FileBrowserMode::Save {
                    ref default_file_name,
                } => self.path.join(default_file_name),
            },
            file_name_value: match self.mode {
                FileBrowserMode::Open => Default::default(),
                FileBrowserMode::Save {
                    ref default_file_name,
                } => default_file_name.clone(),
            },
            filter: self.filter,
            mode: self.mode,
            scroll_viewer,
            root: self.root,
            file_name,
            watcher: Rc::new(cell::Cell::new(None)),
        };
        let watcher = browser.watcher.clone();
        let filebrowser_node = UINode::FileBrowser(browser);
        let node = ctx.add_node(filebrowser_node);
        watcher.replace(setup_filebrowser_fs_watcher(
            ctx.ui.sender(),
            widget_handle,
            the_path,
        ));
        node

Here is the code that sets the FS watcher up (setup_filebrowser_fs_watcher):

fn setup_filebrowser_fs_watcher<M: MessageData, C: Control<M, C>>(
    ui_sender: mpsc::Sender<UiMessage<M, C>>,
    filebrowser_widget_handle: Handle<UINode<M, C>>,
    the_path: PathBuf,
) -> Option<(notify::RecommendedWatcher, thread::JoinHandle<()>)> {
    let (tx, rx) = mpsc::channel();
    match notify::watcher(tx, time::Duration::from_secs(1)) {
        Ok(mut watcher) => {
            #[allow(clippy::while_let_loop)]
            let watcher_conversion_thread = std::thread::spawn(move || loop {
                println!("Waiting for FS Watcher Event....");
                match rx.recv() {
                    Ok(event) => match event {
                        notify::DebouncedEvent::NoticeRemove(path)
                        | notify::DebouncedEvent::Remove(path) => {
                            println!("Sent Remove Message");
                            match ui_sender.send(FileBrowserMessage::remove(
                                filebrowser_widget_handle,
                                MessageDirection::ToWidget,
                                path,
                            )) {
                                Ok(_) => println!("Successfully sent Remove message"),
                                Err(_) => println!("Failed to Send Remove Message"),
                            }
                        }
                        notify::DebouncedEvent::Create(path) => {
                            println!("Sent Create Message");
                            let _ = ui_sender.send(FileBrowserMessage::add(
                                filebrowser_widget_handle,
                                MessageDirection::ToWidget,
                                path,
                            ));
                        }
                        notify::DebouncedEvent::Rescan | notify::DebouncedEvent::Error(_, _) => {
                            println!("Sent Rescan Message");
                            let _ = ui_sender.send(FileBrowserMessage::rescan(
                                filebrowser_widget_handle,
                                MessageDirection::ToWidget,
                            ));
                        }
                        _ => {
                            println!("Ignored FS Watcher Event");
                            ()
                        }
                    },
                    Err(_) => {
                        println!("Breaking out of FS Watcher Event Loop");
                        break;
                    }
                };
            });
            println!(
                "Starting FS Event watch on Path: {}",
                the_path.to_str().unwrap()
            );
            let _ = watcher.watch(the_path, notify::RecursiveMode::Recursive);
            Some((watcher, watcher_conversion_thread))
        }
        Err(_) => None,
    }
}

And then here is the code to handle the messages that are received (in "handle_routed_message"):

       match &message.data() {
            UiMessageData::FileBrowser(msg) => {
                if message.destination() == self.handle() {
                    match msg {
                        FileBrowserMessage::Path(path) => {...}
                        FileBrowserMessage::Root(root) => {...}
                        FileBrowserMessage::Filter(filter) => {...}
                        FileBrowserMessage::Add(path) => {
                            // Check if leading path is expanded, if so, add item to expanded tree
                            println!("Add message received");
                            todo!("FileBrowserMessage::Add Not Implemented");
                        }
                        FileBrowserMessage::Remove(path) => {
                            // Check if leading path is expanded, if so, remove item from expanded tree
                            println!("Remove message received");
                            todo!("FileBrowserMessage::Remove Not Implemented");
                        }
                        FileBrowserMessage::Rescan => {
                            //
                            println!("Rescan message received");
                            todo!("FileBrowserMessage::Rescan Not Implemented");
                        }
                    }
                }
            }
            UiMessageData::TextBox(TextBoxMessage::Text(txt)) => {...}
            UiMessageData::Tree(TreeMessage::Expand { expand, .. }) => {...}
            UiMessageData::TreeRoot(msg) => {...}
            _ => {}
        }

I'm seeing that that the "Add", "Remove" messages are being sent (due to the println!), but the messages are never arriving at the handler. Do I have the right ui sender (ui.ctx.sender())? Do I have the right handle to send to? Am I using the correct API to send the messages? Any thoughts on what could be amiss?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 22, 2021

This let widget_handle = widget.handle(); always returns Handle::NONE until the widget is added to UI by .add_node call. To fix this, remove this line and use node as widget_handle:

let node = ctx.add_node(filebrowser_node);
watcher.replace(setup_filebrowser_fs_watcher(
    ctx.ui.sender(),
    node,
    the_path,
));
node

@gbutler69
Copy link
Contributor

Aaarrrrgh! That did it.

Now, there is another issue I'm not sure how best to resolve given the API. Perhaps you can give me some insight into something I'm not seeing.

When the FileBrowser is built, the working directory is the start-up directory of the rusty-editor. After the widget is built, and as a result, the watcher is setup to watch the rust-editor working directory, then the user selects the working directory and the working directory of the process is changed. Somehow, I need an event that the working directory was changed to get to the FileBrowser so that I can re-setup the watcher to the new directory.

Any thoughts on what would be best given the architecture?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Aug 22, 2021

I think this could be done in handler of FileBrowserMessage::Root message. When user selects new working directory, the editor just tells its file browsers that their root is the new working directory.

@gbutler69
Copy link
Contributor

gbutler69 commented Sep 2, 2021

Hello again. I'm running into a problem understanding the API for interacting with the Browser Tree. I have added the following code to handle the "Add" event for the FS Notifier:

In file_browser.rs:

match msg {
                        FileBrowserMessage::Path(path) => {...}
                        FileBrowserMessage::Root(root) => {...}
                        FileBrowserMessage::Filter(filter) => {...}
                        FileBrowserMessage::Add(path) => {
                            if let Some(filter) = self.filter.as_mut() {
                                if !filter.0.borrow_mut().deref_mut().lock().unwrap()(path) {
                                    return;
                                }
                            }
                            let mut parent_path = path.clone();
                            parent_path.pop();
                            let existing_parent_node = find_tree(self.tree_root, &parent_path, ui);
                            if existing_parent_node.is_some() {
                                build_tree(
                                    existing_parent_node,
                                    existing_parent_node == self.tree_root,
                                    path,
                                    &parent_path,
                                    ui,
                                );
                            }
                        }
                        FileBrowserMessage::Remove(_path) => {
                            println!("FileBrowserMessage::Remove Received and Ignored");
                        }
                        FileBrowserMessage::Rescan => {
                            println!("FileBrowserMessage::Rescan Received and Ignored");
                        }
                    }

When I call the find_tree method it doesn't do what I would expect or what the code seems to indicate it is trying to do. I'm expecting it to find/traverse the tree looking for an existing node that has the same path as the input path. However, it doesn't get beyond the root (".") level where it shows that the root node has no "items" even though the root node (.) has underneath it: /git, /assets, /src, etc. Am I calling/using "find_tree" incorrectly? Here is the code for "find_tree":

fn find_tree<M: MessageData, C: Control<M, C>, P: AsRef<Path>>(
    node: Handle<UINode<M, C>>,
    path: &P,
    ui: &UserInterface<M, C>,
) -> Handle<UINode<M, C>> {
    let mut tree_handle = Handle::NONE;
    match ui.node(node) {
        UINode::Tree(tree) => {
            let tree_path = tree.user_data_ref::<PathBuf>().unwrap();
            if tree_path == path.as_ref() {
                tree_handle = node;
            }
            for &item in tree.items() {
                let tree = find_tree(item, path, ui);
                if tree.is_some() {
                    tree_handle = tree;
                    break;
                }
            }
        }
        UINode::TreeRoot(root) => {
            for &item in root.items() {
                let tree = find_tree(item, path, ui);
                if tree.is_some() {
                    tree_handle = tree;
                    break;
                }
            }
        }
        _ => unreachable!(),
    }
    tree_handle
}

When I step through with the debugger it goes into "find_tree", then to the "TreeRoot" of find_tree which then performs "find_tree" on the "Items" where the only item is the "." node. In the recursive call to find_tree it goes to the "Tree" route sees that the tree_path is ".", then proceeds to process the items but "items" is empty (even though the UI shows multiple children that are displayed and open). Any thoughts on what I'm doing wrong?

Here is what the debugger shows when it stops at the for &item in tree.items() line in find_tree:

image

Meanwhile, here is what the UI shows:

image

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 2, 2021

Hi! At first, I wrote a test to ensure that find_tree is working correctly - 794a38a. And it does.

The items is empty because tree's descendant nodes aren't build until user expand the parent item. I'm pretty sure that you're looking at /src item the debugger screenshot. If you look at it when it is expanded, it should contain all the items.

Your code seems to be correct, it should add items only to expanded tree items, it makes no sense to add to collapsed because a user won't see them.

@gbutler69
Copy link
Contributor

I'm pretty sure that you're looking at /src item the debugger screenshot

It doesn't seem like it. Notice that the "tree_path" local variable is "." not "./src" or "src". Also, it got here by a single recursive call from the "TreeRoot" route which, from my reading of the code, never has an associated "pathBuf". So, the first and only node of the "TreeRoot" is the "." node. As can be seen from the debug output, "items" is empty on the "." node even though the UI clearly shows children. It should be iterating over "src", "assets", "git", etc. Instead, it iterates over nothing.

It almost seems like there are two ui components, the one in the actual UI, and the one in the "FileBrowser" struct and they are out-of-sync. As if it were cloned, but, I can't see where that would be occurring.

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 2, 2021

It is hard to tell what is wrong here, I checked the code and it seems to work fine. I mean I added a simple resursive print func that prints paths of trees (basically just stripped version of find_tree) and it correctly prints all the paths in the entire hierarchy. I need to see all your changes, if you have a repo, I could take a look.

@gbutler69
Copy link
Contributor

gbutler69 commented Sep 2, 2021

Sure thing. Here is a link to my GitHUB repo/fork of rg3d (https://github.com/gbutler69/rg3d) where I'm working on this. My intent was to sync up once I had it working and create a PR.

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 2, 2021

Ok, I'm confused about path in the debugger screenshot - it seems that it is an absolute path, but tree starts from working directory (.). find_tree just looks for an item that has matching path, but none of file browser's items has anything similar to the path's value.

find_tree in your case will work only if file browser does not have custom root (in case when it shows the entire file system starting from root of every drive). In case when root is set, you need to strip it from parent_path to make find_tree working.

@gbutler69
Copy link
Contributor

gbutler69 commented Sep 2, 2021

Ok, I'm confused about path in the debugger screenshot

Yes, I know I'll need to strip off the root path when attempting to match the path; however, that doesn't change the fact that "items()" is empty in the "." node of the UI instead of having sub-nodes "src", "git", "assets", etc. Until that is resolved, it doesn't matter whether "path" is relative or absolute because there is no comparison happening.

When you run the code from my repo, what shows in the UI? Does it appear differently than it does for me?

The problem right now is not that the comparison fails but that the comparison of "path" to items.paths never occurs because "items()" is empty on the "." node. How can that be? What do you see when you run it?

This is the procedure I'm using:

  1. Compile for debug
  2. Have break-points set at every call to "find_tree" and at the top of loops within "find_tree"
  3. Start in debugger
  4. Select the project working directory ("/raid/home/gbutler/Repos/Git/rg3d/rg3d-shooter")
  5. Expand the "." node of the FileBrowser (which displays the "src", "git", "assets", etc sub-folders)
  6. Add a new file to the "assets" folder within the "rg3d-shooter" folder
  7. Step through until find_tree recursively calls itself for the first time from the "TreeRoot" route
  8. Step through until the top of the loop within the "Tree" route
  9. Check the value of "tree" and "tree_path".

You'll notice that at this point tree.items is empty and tree_path is ".". What should happen is that tree.items should include an entry for each of the sub-folders of "rg3d-shooter" that is currently displayed in the UI. That does not happen. So, no comparison can occur. Getting the tree.items to show correctly is the first step to make this work.

What are you seeing when you run it? I'm befuddled at the behavior and can't make sense of how find_tree seems to work in other contexts. It really seems like the tree being searched isn't the tree showing in the UI. As if a Clone were performed before expanding so that the "tree_root" stored in the "FileBrowser" structure is before expansion. Does any of that ring any bells?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 3, 2021

I managed to figure out what happens and why it "not working". But let's start from the beginning:

  1. rusty-editor has multiple (at least 8) file browsers created on start, so debugging isn't quite accurate - you can't be sure that you're looking at expected file browser.
  2. Almost every FileBrowser in rusty-editor has filter - either on .rgs extenstion (for scene browser) or on is_dir (asset browser has it).
  3. In FileBrowser::Add message handler you check if new path can pass filter, but it asset browser's file browser has is_dir filter, but I'm pretty sure that you're creating a txt or something instead of directory.
  4. If you remove filtering at FileBrowser::Add, then you'll see that everything works with any kind of files.

@gbutler69
Copy link
Contributor

@mrDIMAS : OK, I have the add functionality mostly working. It correctly adds a new node to an open folder and does not add an item to a non-open folder; however, if a folder is initially empty, it is displayed in the widget without an expander. If you add an item to that folder, even though you don't want to display the new item, you want to add an expander to the existing folder that is displayed so that the user now knows that it can be expanded (there is something underneath it now). I tried adding the following code to the "tree" widget implementation for message handling:

In tree.rs:

                        &TreeMessage::SetExpanderShown(show) => {
                            self.always_show_expander = show;
                        }

I send this message from the FileBrowser widget when an item is added to a folder that is displayed, but, was empty and so has no expander and is not currently expanded. This should then display the expander. The expander does not display. Is there something I need to call so that it knows to refresh the rendering?

I've committed all these changes to my repo that you looked at before (https://github.com/gbutler69/rg3d).

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 4, 2021

Cool, great job!

To make SetExpanderShown handler to work correctly, you need to call self.invalidate_arrange() to force UI to do an arragement pass which will call arrange_override handler which in its turn show expander.

The reason why it needs manual triggering, is because layout is very heavy and it is cached as much as possible.

@gbutler69
Copy link
Contributor

@mrDIMAS : OK, but, I found where the expander button is built and it is the following code:

from tree.rs:

ButtonBuilder::new(
        WidgetBuilder::new()
            .with_width(20.0)
            .with_visibility(always_show_expander || items_populated)
            .on_row(0)
            .on_column(0),
    )
    .with_text(if is_expanded { "-" } else { "+" })
    .build(ctx)

This seems to indicate that I'll need to somehow set the button's visibility property before doing the arrange_override, or will the arrange_override cause that to recalculated somewhere already?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 4, 2021

You just need to call self.invalidate_arrange() after you add an item to an empty tree and everything else will be done automatically:

fn arrange_override(&self, ui: &UserInterface<M, C>, final_size: Vector2<f32>) -> Vector2<f32> {
        let size = self.widget.arrange_override(ui, final_size);

        if !self.always_show_expander {
            let expander_visibility = !self.items.is_empty();
            ui.send_message(WidgetMessage::visibility(
                self.expander,
                MessageDirection::ToWidget,
                expander_visibility,
            ));
        }

        size
    }

Is you can see, expander will be shown if the tree has some items.

@gbutler69
Copy link
Contributor

@mrDIMAS : I should've looked for arrange_override before I questioned what you were saying. I have it working now, however, I needed to make the following changes to arrange_override:

fn arrange_override(&self, ui: &UserInterface<M, C>, final_size: Vector2<f32>) -> Vector2<f32> {
        let size = self.widget.arrange_override(ui, final_size);

        let expander_visibility = !self.items.is_empty() || self.always_show_expander;
        ui.send_message(WidgetMessage::visibility(
            self.expander,
            MessageDirection::ToWidget,
            expander_visibility,
        ));

        size
    }

Do you consider this to be a problem at all?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 4, 2021

It is fine. There is small drawback of this - it will send a message on each arrange call of tree, but I think it shouldn't be an issue, because by default always_show_expander is false.

@gbutler69
Copy link
Contributor

OK, sounds good. That aligns with what I was thinking as well. I think I'm ready to create a PR for the "Add" portion or would you prefer we wait until Add & Remove is fully functional to have a PR?

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 4, 2021

I'm not in a hurry, I think adding a Remove functionaly woudn't be too hard so take your time.

@gbutler69
Copy link
Contributor

@mrDIMAS : Pull Request to resolve this issue - #176

@mrDIMAS
Copy link
Member Author

mrDIMAS commented Sep 5, 2021

Resolved in #176

@mrDIMAS mrDIMAS closed this as completed Sep 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request user-interface
Projects
None yet
Development

No branches or pull requests

2 participants