Change druid Image Widget with lens

1.3k Views Asked by At

I have been using druid for my newest project, and I'm really enjoying the simplicity of the model. However I'm struggling to use lenses with an image, and the docs unfortunately don't include the docs for the image feature.

I was considering writing my own lens, but I don't want to clone a bunch of data around after unlocking. I'm using the github dependency path, so whatever version they're currently on.

#[derive(Clone, Default, Lens)]
pub struct State {
    pub display_image: Option<Arc<RwLock<ImageBuf>>>,
    ...
}


...
let image = ViewSwitcher::new(
    |data: &State, _env| data.display_image.is_some(),
    move |f, data: &State, _env| {
        if *f {
            Box::new(
                //if I don't use Arc<RwLock<?>> here, the compiler complains that Imgbuf doesn't impl Data
                //The initial image is shown, but when Imgbuf changes, the widget is not updated
                //I would imagine this has to do with it being an Arc<RwLock<?>>, but I'm not sure how to make druid and rust happy at the same time
                //Is there a wrapper widget available to give me the option of using a closure to update instead of a lens? Like how Label::dynamic(|data, env|) works?
                Image::new(data.display_image.as_ref().unwrap().read().unwrap().clone())
                    .lens(State::display_image),
            )
        } else {
            //Placeholder svg. Completely static. Works well!
            Box::new(Svg::new(svg).fill_mode(FillStrat::Fill))
        }
    },
);
...

Important links:

2

There are 2 best solutions below

0
On

I "fixed" this using the painter widget, with the drawback that I'm having to handle aspect ratios and sizing on my own. I would love an answer that uses the Image widget, because the source code says it handles sizing and aspect ratios.

let image = ViewSwitcher::new(
    |data: &State, _env| data.base_state.is_some() || !data.wrapper_state.is_empty(),
    move |f, _data: &State, _env| {
        if *f {
            Box::new(Painter::new(|ctx, data: &State, _env| {
                if data.is_changed() {
                    let rect = ctx.size().to_rect();
                    let img = ImageBuf::from_dynamic_image(
                        data.display_image.as_ref().unwrap().read().unwrap().clone(),
                    );
                    let img = img.to_piet_image(ctx.render_ctx);
                    ctx.with_save(|ctx| {
                        ctx.draw_image(&img, rect, piet::InterpolationMode::Bilinear);
                    });
                    data.set_changed(false);
                }
            }))
        } else {
            Box::new(Svg::new(svg.clone()).fill_mode(FillStrat::Fill))
        }
    },
);
0
On

Disclaimer

This answer is based on the current Druid version 0.7.0. When this question was asked, Druid 0.7.0 was not yet released. Nonetheless, there should probably not be that many differences between the two versions in regards to the question asked.


TL,DR: Never use interior mutability types like RwLock as part of a trait implementing Data. Best case, it won't compile, worst case (if it is wrapped inside an Arc or a Rc), it will compile, but ignore updates at runtime – that's what is happening here. Instead, wrap the type inside an Arc and use Arc::make_mut() for modifications. The first closure given to ViewSwitcher::new also needs to be modified so that changes from Some(x) to Some(y) are detected, see "The second problem" below.


I will discuss this problem with this piece of code:

use druid::widget::{ViewSwitcher, FillStrat, Image, Svg, Flex};
use druid::Widget;
use druid::Lens;
use druid::Data;
use druid::AppLauncher;
use druid::WidgetExt;
use druid::WindowDesc;
use druid::ImageBuf;
use druid::widget::Button;
use std::sync::RwLock;
use std::sync::Arc;
use druid::EventCtx;
use druid::Env;


#[derive(Clone, Default, Lens, Data)]
pub struct State {
    pub display_image: Option<Arc<RwLock<ImageBuf>>>
}

fn main() {
    // describe the main window
    let main_window = WindowDesc::new(build_root_widget)
        .title("Test")
        .window_size((400.0, 600.0));

    // create the initial app state
    let initial_state = State {
        display_image: None
    };

    // start the application
    AppLauncher::with_window(main_window)
        .launch(initial_state)
        .expect("Failed to launch application");
}

fn build_root_widget() -> impl Widget<State> {
    Flex::column()
        .with_child(ViewSwitcher::new(
            |data: &State, _env| data.display_image.is_some(),
            move |f, data: &State, _env| {
                if *f {
                    Box::new(
                        Image::new(data.display_image.as_ref().unwrap().read().unwrap().clone())
                            .lens(State::display_image),
                    )
                } else {
                    //Placeholder svg. Completely static. Works well!
                    Box::new(Svg::new("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>
    <!-- Created with Inkscape (http://www.inkscape.org/) -->
    <svg xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" width=\"225\" height=\"150\" id=\"svg2\">
      <defs id=\"defs5\"/>
      <rect x=\"0\" y=\"0\" width=\"225\" height=\"150\" style=\"fill:#d4d4d4\" id=\"rect1310\"/>
      <path d=\"M 64.255859,62.075068 L 63.136719,62.075068 C 63.128904,62.047727 63.092772,61.948118 63.02832,61.77624 C 62.963866,61.604368 62.931639,61.453977 62.931641,61.325068 C 62.931639,61.078978 62.965819,60.84265 63.03418,60.616084 C 63.102537,60.389525 63.203123,60.177611 63.335938,59.980341 C 63.468748,59.783081 63.731443,59.467651 64.124023,59.034052 C 64.516598,58.600465 64.712887,58.243043 64.712891,57.961787 C 64.712887,57.414919 64.355466,57.141482 63.640625,57.141474 C 63.292967,57.141482 62.929686,57.31531 62.550781,57.662959 L 61.947266,56.532099 C 62.451171,56.137576 63.113279,55.940311 63.933594,55.940302 C 64.566403,55.940311 65.094723,56.116092 65.518555,56.467646 C 65.942378,56.819216 66.154292,57.286013 66.154297,57.868037 C 66.154292,58.266481 66.077144,58.603394 65.922852,58.878779 C 65.76855,59.154175 65.497066,59.477417 65.108398,59.848505 C 64.719723,60.219604 64.466794,60.528197 64.349609,60.774287 C 64.232419,61.020384 64.173825,61.289915 64.173828,61.58288 C 64.173825,61.645383 64.201169,61.809446 64.255859,62.075068 L 64.255859,62.075068 z M 63.757813,62.871943 C 64.023435,62.871945 64.249021,62.965695 64.43457,63.153193 C 64.620114,63.340694 64.712887,63.567257 64.712891,63.83288 C 64.712887,64.098506 64.620114,64.325068 64.43457,64.512568 C 64.249021,64.700068 64.023435,64.793818 63.757813,64.793818 C 63.492185,64.793818 63.265623,64.700068 63.078125,64.512568 C 62.890623,64.325068 62.796874,64.098506 62.796875,63.83288 C 62.796874,63.567257 62.890623,63.340694 63.078125,63.153193 C 63.265623,62.965695 63.492185,62.871945 63.757813,62.871943 L 63.757813,62.871943 z \" transform=\"matrix(10.52848,0,0,10.52848,-561.8574,-560.5734)\" style=\"font-size:12px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:white;font-family:Trebuchet MS\" id=\"flowRoot1875\"/>
    </svg>".parse().unwrap()).fill_mode(FillStrat::Fill))
                }
            },
        ))
        .with_spacer(50.0)
        .with_child(Button::new("Click me")).on_click(|_: &mut EventCtx, data: &mut State, _: &Env| {
            use rand::Rng;
            use druid::piet::ImageFormat;
            let new_image_buf = ImageBuf::from_raw(vec!(rand::thread_rng().gen(), rand::thread_rng().gen(), rand::thread_rng().gen()), ImageFormat::Rgb, 1, 1);
            match &data.display_image {
                None => data.display_image = Some(Arc::new(RwLock::new(new_image_buf))),
                Some(val) => *val.as_ref().write().unwrap() = new_image_buf
            }
        })

(Fallback SVG taken from https://commons.wikimedia.org/wiki/File:Flag_of_None.svg – public domain)

This is a really basic application with one button that displays a one Pixel image above the button (scaled up to 400x400 px to keep the aspect ratio).

There are two problems in this code that I will discuss below.

The first problem

Let's first look at some theory: Data is a trait that has only one method named same(). This method is used by Druid to check whether a struct implementing Data was changed by some user-supplied function:

  1. the struct is cloned and saved for later reference
  2. the user-supplied function is called with a reference to the original struct
  3. the Data::same() method is used to check whether the original struct and the clone are the same
  4. if same() returns false, the data was changed by the user-supplied function and update() is called on all Widgets that have access to the data

Not relevant for this question, but just for the sake of completeness: same() may return false even if there were no changes, resulting in superfluous calls to update().

Now to the example: ImageBuf does not impl Data, therefore it is wrapped as Arc<RwLock<ImageBuf>>. If you look at the implementation of same() for Arc, you can see that sameness is computed by checking whether the Arcs point to the same allocation. This is the case in your example: The RwLock inside your Arc is the same before as well as after the modification. Of course, the contents of the RwLock, the ImageBuf, has changed completely, but the same() implementation of Arc can't see this.

Solution to the first problem

You cannot use RwLock here (or any other interior mutability type). Instead, the idiomatic Druid way is to wrap the type directly in an Arc and call Arc::make_mut() to modify it. This is what is done in one of the examples in the Druid book, although the Druid book is not very explicit about this being the idiomatic way – this is a known problem. This is also what is done in a Druid tutorial (in my opinion, worth a read for Druid beginners).

The code now looks like this:

use druid::widget::{ViewSwitcher, FillStrat, Image, Svg, Flex};
use druid::Widget;
use druid::Lens;
use druid::Data;
use druid::AppLauncher;
use druid::WidgetExt;
use druid::WindowDesc;
use druid::ImageBuf;
use druid::widget::Button;
use std::sync::Arc;
use druid::EventCtx;
use druid::Env;


#[derive(Clone, Default, Lens, Data)]
pub struct State {
    pub display_image: Option<Arc<ImageBuf>>
}

fn main() {
    // describe the main window
    let main_window = WindowDesc::new(build_root_widget)
        .title("Test")
        .window_size((400.0, 600.0));

    // create the initial app state
    let initial_state = State {
        display_image: None
    };

    // start the application
    AppLauncher::with_window(main_window)
        .launch(initial_state)
        .expect("Failed to launch application");
}

fn build_root_widget() -> impl Widget<State> {
    Flex::column()
        .with_child(ViewSwitcher::new(
            |data: &State, _env| data.display_image.is_some(),
            move |f, data: &State, _env| {
                if *f {
                    Box::new(
                        //if I don't use Arc<RwLock<?>> here, the compiler complains that Imgbuf doesn't impl Data
                        //The initial image is shown, but when Imgbuf changes, the widget is not updated
                        //I would imagine this has to do with it being an Arc<RwLock<?>>, but I'm not sure how to make druid and rust happy at the same time
                        //Is there a wrapper widget available to give me the option of using a closure to update instead of a lens? Like how Label::dynamic(|data, env|) works?
                        Image::new(data.display_image.as_ref().unwrap().as_ref().clone())
                            .lens(State::display_image),
                    )
                } else {
                    //Placeholder svg. Completely static. Works well!
                    Box::new(Svg::new("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>
    <!-- Created with Inkscape (http://www.inkscape.org/) -->
    <svg xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" width=\"225\" height=\"150\" id=\"svg2\">
      <defs id=\"defs5\"/>
      <rect x=\"0\" y=\"0\" width=\"225\" height=\"150\" style=\"fill:#d4d4d4\" id=\"rect1310\"/>
      <path d=\"M 64.255859,62.075068 L 63.136719,62.075068 C 63.128904,62.047727 63.092772,61.948118 63.02832,61.77624 C 62.963866,61.604368 62.931639,61.453977 62.931641,61.325068 C 62.931639,61.078978 62.965819,60.84265 63.03418,60.616084 C 63.102537,60.389525 63.203123,60.177611 63.335938,59.980341 C 63.468748,59.783081 63.731443,59.467651 64.124023,59.034052 C 64.516598,58.600465 64.712887,58.243043 64.712891,57.961787 C 64.712887,57.414919 64.355466,57.141482 63.640625,57.141474 C 63.292967,57.141482 62.929686,57.31531 62.550781,57.662959 L 61.947266,56.532099 C 62.451171,56.137576 63.113279,55.940311 63.933594,55.940302 C 64.566403,55.940311 65.094723,56.116092 65.518555,56.467646 C 65.942378,56.819216 66.154292,57.286013 66.154297,57.868037 C 66.154292,58.266481 66.077144,58.603394 65.922852,58.878779 C 65.76855,59.154175 65.497066,59.477417 65.108398,59.848505 C 64.719723,60.219604 64.466794,60.528197 64.349609,60.774287 C 64.232419,61.020384 64.173825,61.289915 64.173828,61.58288 C 64.173825,61.645383 64.201169,61.809446 64.255859,62.075068 L 64.255859,62.075068 z M 63.757813,62.871943 C 64.023435,62.871945 64.249021,62.965695 64.43457,63.153193 C 64.620114,63.340694 64.712887,63.567257 64.712891,63.83288 C 64.712887,64.098506 64.620114,64.325068 64.43457,64.512568 C 64.249021,64.700068 64.023435,64.793818 63.757813,64.793818 C 63.492185,64.793818 63.265623,64.700068 63.078125,64.512568 C 62.890623,64.325068 62.796874,64.098506 62.796875,63.83288 C 62.796874,63.567257 62.890623,63.340694 63.078125,63.153193 C 63.265623,62.965695 63.492185,62.871945 63.757813,62.871943 L 63.757813,62.871943 z \" transform=\"matrix(10.52848,0,0,10.52848,-561.8574,-560.5734)\" style=\"font-size:12px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:white;font-family:Trebuchet MS\" id=\"flowRoot1875\"/>
    </svg>".parse().unwrap()).fill_mode(FillStrat::Fill))
                }
            },
        ))
        .with_spacer(50.0)
        .with_child(Button::new("Click me")).on_click(|_: &mut EventCtx, data: &mut State, _: &Env| {
            use rand::Rng;
            use druid::piet::ImageFormat;
            let new_image_buf = ImageBuf::from_raw(vec!(rand::thread_rng().gen(), rand::thread_rng().gen(), rand::thread_rng().gen()), ImageFormat::Rgb, 1, 1);
            match &mut data.display_image {
                None => data.display_image = Some(Arc::new(new_image_buf)),
                Some(val) => *Arc::make_mut(val) = new_image_buf
            }
        })

If you execute this, you will see that it still does not work.

The second problem

Let's look at the construction of the ViewSwitcher:

ViewSwitcher::new(
    |data: &State, _env| data.display_image.is_some(),
    move |f, data: &State, _env| {
        // ...
    },

From the documentation:

The [first] closure is called every time the application data changes. If the value it returns is the same as the one it returned during the previous data change, nothing happens. If it returns a different value, then the child_builder closure is called with the new value.

Highlighting by me. This means that a new image is only generated if the result of data.display_image.is_some() changes – so if display_image changes from Some to None or back. This is not what you want – changes from Some(x) to Some(y) (with x and y being different ImageBufs) do not result in generating a new image. You can instead simply return data.clone() (if data is big, you could instead call data.display_image.clone() to avoid cloning all the irrelevant state).

The code now looks like this (there are some more changes for the second closure to work with the modified first closure):

use druid::widget::{ViewSwitcher, FillStrat, Image, Svg, Flex};
use druid::Widget;
use druid::Lens;
use druid::Data;
use druid::AppLauncher;
use druid::WidgetExt;
use druid::WindowDesc;
use druid::ImageBuf;
use druid::widget::Button;
use std::sync::Arc;
use druid::EventCtx;
use druid::Env;


#[derive(Clone, Default, Lens, Data)]
pub struct State {
    pub display_image: Option<Arc<ImageBuf>>
}

fn main() {
    // describe the main window
    let main_window = WindowDesc::new(build_root_widget)
        .title("Test")
        .window_size((400.0, 600.0));

    // create the initial app state
    let initial_state = State {
        display_image: None
    };

    // start the application
    AppLauncher::with_window(main_window)
        .launch(initial_state)
        .expect("Failed to launch application");
}

fn build_root_widget() -> impl Widget<State> {
    Flex::column()
        .with_child(ViewSwitcher::new(
            |data: &State, _env| data.clone(),
            move |_, data: &State, _env| {
                if data.display_image.is_some() {
                    Box::new(
                        //if I don't use Arc<RwLock<?>> here, the compiler complains that Imgbuf doesn't impl Data
                        //The initial image is shown, but when Imgbuf changes, the widget is not updated
                        //I would imagine this has to do with it being an Arc<RwLock<?>>, but I'm not sure how to make druid and rust happy at the same time
                        //Is there a wrapper widget available to give me the option of using a closure to update instead of a lens? Like how Label::dynamic(|data, env|) works?
                        Image::new(data.display_image.as_ref().unwrap().as_ref().clone())
                            .lens(State::display_image),
                    )
                } else {
                    //Placeholder svg. Completely static. Works well!
                    Box::new(Svg::new("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>
    <!-- Created with Inkscape (http://www.inkscape.org/) -->
    <svg xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" width=\"225\" height=\"150\" id=\"svg2\">
      <defs id=\"defs5\"/>
      <rect x=\"0\" y=\"0\" width=\"225\" height=\"150\" style=\"fill:#d4d4d4\" id=\"rect1310\"/>
      <path d=\"M 64.255859,62.075068 L 63.136719,62.075068 C 63.128904,62.047727 63.092772,61.948118 63.02832,61.77624 C 62.963866,61.604368 62.931639,61.453977 62.931641,61.325068 C 62.931639,61.078978 62.965819,60.84265 63.03418,60.616084 C 63.102537,60.389525 63.203123,60.177611 63.335938,59.980341 C 63.468748,59.783081 63.731443,59.467651 64.124023,59.034052 C 64.516598,58.600465 64.712887,58.243043 64.712891,57.961787 C 64.712887,57.414919 64.355466,57.141482 63.640625,57.141474 C 63.292967,57.141482 62.929686,57.31531 62.550781,57.662959 L 61.947266,56.532099 C 62.451171,56.137576 63.113279,55.940311 63.933594,55.940302 C 64.566403,55.940311 65.094723,56.116092 65.518555,56.467646 C 65.942378,56.819216 66.154292,57.286013 66.154297,57.868037 C 66.154292,58.266481 66.077144,58.603394 65.922852,58.878779 C 65.76855,59.154175 65.497066,59.477417 65.108398,59.848505 C 64.719723,60.219604 64.466794,60.528197 64.349609,60.774287 C 64.232419,61.020384 64.173825,61.289915 64.173828,61.58288 C 64.173825,61.645383 64.201169,61.809446 64.255859,62.075068 L 64.255859,62.075068 z M 63.757813,62.871943 C 64.023435,62.871945 64.249021,62.965695 64.43457,63.153193 C 64.620114,63.340694 64.712887,63.567257 64.712891,63.83288 C 64.712887,64.098506 64.620114,64.325068 64.43457,64.512568 C 64.249021,64.700068 64.023435,64.793818 63.757813,64.793818 C 63.492185,64.793818 63.265623,64.700068 63.078125,64.512568 C 62.890623,64.325068 62.796874,64.098506 62.796875,63.83288 C 62.796874,63.567257 62.890623,63.340694 63.078125,63.153193 C 63.265623,62.965695 63.492185,62.871945 63.757813,62.871943 L 63.757813,62.871943 z \" transform=\"matrix(10.52848,0,0,10.52848,-561.8574,-560.5734)\" style=\"font-size:12px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:white;font-family:Trebuchet MS\" id=\"flowRoot1875\"/>
    </svg>".parse().unwrap()).fill_mode(FillStrat::Fill))
                }
            },
        ))
        .with_spacer(50.0)
        .with_child(Button::new("Click me")).on_click(|_: &mut EventCtx, data: &mut State, _: &Env| {
            use rand::Rng;
            use druid::piet::ImageFormat;
            let new_image_buf = ImageBuf::from_raw(vec!(rand::thread_rng().gen(), rand::thread_rng().gen(), rand::thread_rng().gen()), ImageFormat::Rgb, 1, 1);
            match &mut data.display_image {
                None => data.display_image = Some(Arc::new(new_image_buf)),
                Some(val) => *Arc::make_mut(val) = new_image_buf
            }
        })
}

If you run this, you should see a new random image after every button press.

One additional observation

Here:

Image::new(data.display_image.as_ref().unwrap().as_ref().clone()).lens(State::display_image)

The call to lens() is superfluous. Image does not work with State anyway.