Skip to content

Provide ctx in the draw closure of Canvas#1552

Merged
xStrom merged 4 commits intolinebender:mainfrom
DaraJKong:main
Jan 14, 2026
Merged

Provide ctx in the draw closure of Canvas#1552
xStrom merged 4 commits intolinebender:mainfrom
DaraJKong:main

Conversation

@DaraJKong
Copy link
Copy Markdown
Contributor

In a project I'm working on, I needed to draw text inside a Canvas widget. I initially created a FontContext and a LayoutContext inside the draw closure. I used masonry::core::render_text to render my text layout to the Vello Scene. While theoretically functionnal, my solution was extremely slow and wrong.

After realizing that both FontContext and LayoutContext are meant to be global resources created only once per application, I set out to tinker with the source code of the Canvas widget. What I came up with allowed me to do this:

canvas(|state: &mut AppState, ctx, scene, size| {
    let (fcx, lcx) = ctx.text_contexts();

    let mut text_layout_builder = lcx.ranged_builder(fcx, "Text", 1., true);
    text_layout_builder.push_default(StyleProperty::FontStack(FontStack::Single(
        FontFamily::Generic(GenericFamily::Serif),
    )));
    text_layout_builder.push_default(StyleProperty::FontSize(24.));

    let mut text_layout = text_layout_builder.build("Text");
    text_layout.break_all_lines(None);
    text_layout.align(None, TextAlign::Start, TextAlignOptions::default());

    masonry::core::render_text(
        scene,
        Affine::IDENTITY,
        &text_layout,
        &[css::WHITE.into()],
        true,
    );
})

I updated the FnOnce closure to accept a new ctx parameter. Then in Masonry, inside the Canvas::update_scene function, I got the ctx from the WidgetMut<'_, Canvas> and passed it as a mutable reference to the closure. In Xilem, I simply pass the ctx from the closure to the draw closure.

This PR includes the addition of a new screenshot test. The text_canvas test renders a 200x200 image with the text 'Canvas' stretched to fit inside.

Comment on lines +56 to +59
pub fn update_scene(
this: &mut WidgetMut<'_, Self>,
f: impl FnOnce(&mut MutateCtx<'_>, &mut Scene, Size),
) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using PaintCtx would make more sense?

MutateCtx can do a lot of things that this widget wouldn't need.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but that isn't really a problem with this PR per-se. This whole update_scene method seems weird. Why isn't this user-provided function a field of the widget struct that then gets called in paint?

I haven't read through the four canvas PRs, maybe I should do that to find an answer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't read through the four canvas PRs, maybe I should do that to find an answer.

I've explained it in #1519 (comment)

Regarding context type, I think it might even make sense to create a separate type specifically for this function that has a limited API.

But to use a PaintCtx here, something like this is necessary in masonry_core (as the fields are private):

impl MutateCtx<'_> {
    pub fn paint_mut(&mut self) -> PaintCtx<'_> {
        PaintCtx {
            global_state: self.global_state,
            widget_state: self.widget_state,
            children: self.children.reborrow_mut(),
        }
    }
}

Copy link
Copy Markdown
Contributor

@PoignardAzur PoignardAzur Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But to use a PaintCtx here, something like this is necessary in masonry_core (as the fields are private):

I... kind of don't want to add that.

If we don't have an immediate solution, we might want to accept this PR now and rethink the canvas API later. I do think the current API is a bit weird (why pass a callback instead of a scene directly?), but this PR isn't responsible for it.

@DaraJKong
Copy link
Copy Markdown
Contributor Author

To clarify, I don't know if this is the correct way to do it as it's my first PR for this repo. But it works without issues for my use case on Linux.

@xStrom
Copy link
Copy Markdown
Member

xStrom commented Jan 13, 2026

The PR has a merge conflict that needs to be resolved. I suggest rebasing on main.

Copy link
Copy Markdown
Member

@xStrom xStrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah let's merge this and open a new issue for the context type.

@xStrom xStrom added this pull request to the merge queue Jan 14, 2026
Merged via the queue into linebender:main with commit 6fc8951 Jan 14, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants