I need some help for my crate shoping list (2D graphics and GUI)

Hello. I’m building a tool whose goal is to analyze a crate, and display in a graphic fashion information about it’s internal. At the moment it’s mostly the callgraph (both direct call and through function pointer / Fn traits), as well as displaying the places where a function pointer (or similar) is created. My prototype is working well. I’m using graphviz to generate the graph but this is very impractical, because I want to interactively display a very small subset of the whole graph (let say 10-30 functions and their relations among the 50k+ functions and relation of a crate). Let me give you a mock-up:

Note: the black dotted box and the big black circles are just construction traits, they shouldn’t appear in the real rendering.

  • I want to be able to move (using a mouse/keyboard/trackpad) the boxes. It should make the text inside, as well as the end of the arrows move together.
  • The arrow (both solid or dotted) outside of the boxes should never cross another box, and minimize superposition between themselves (ideally they should cross at 90°) for maximum readability.
  • I want to be able to add an remove the set of visible functions (and this means recomputing the optimal path for the arrows).
  • I need to be able to display text with colors, and access it’s exact coordinates of the substrings to create the arrows (where they start / finish / merge) as well as the collision boxes for the functions.

I’m looking for crates that could help me generates such drawing, especially for the interactive part, and the routing / collision detection of the arrows.

Given that I have never programmed any GUI or 2D graphics, I think that "obvious" things are not necessarily obvious for me :frowning: . I want to be able to everything in Rust. If possible, and if it doesn’t increase the complexity too much, I’d like to be able to use webassembly to have a pre-compiled binary that is easy too share, but that’s really not a hard requirement. I never used webassembly, nor have any previous experience with javascript.

In a few weeks, I'll probably have a crate that can help you out a lot in this direction, but as text rendering is this week's project it's not quite there yet: flo_draw is a library for rendering 2D graphics quickly. The follow_mouse demonstrates how to build basic interactive applications on top of the framework, and I'm working on a guide for the next version.

That version will also have support for rendering text, which I think is the main thing you'd be looking for that's currently missing. I am planning on supporting webassembly eventually, but the current version is built around popping up desktop windows (and rendering to byte buffers). Thinking about it, I want to add SVG as an offscreen render target and that can probably be wrangled into webassembly support quite quickly. There's also a javascript implementation that reads encoded instruction used in FlowBetween that could probably be made to work with a webassembly crate (though you'd have to figure out how to connect it up yourself for now).

I've another crate that may help you: flo_curves is full of routines for dealing with bezier curves and in particular can tell you if and where two collide using the curve_intersects_curve_clip() function. There's also a fit_curve() function for making curves that go through a particular set of points and a move_point() function for adjusting existing curves. It's not quite what you need to lay out a graph, but it's a lot of what you might need for adjusting your rendering so the lines don't overlap each other.

3 Likes

Unfortunately GUI is a weak area for Rust right now. There are many nascent GUI crates but none that is very mature. Features change frequently, and much documentation is out of date. You may be better off using an existing GUI toolkit (there are many excellent JavaScript projects targeting the web browser) with Rust on the backend.

If you want to use mostly Rust and are targeting WebAssembly in the browser, seed and yew are the most mature frameworks and are typically deployed using trunk. I don't have personal experience with higher-level GUI crates that do the same thing as Graphviz, although it looks like @ahunter would like to collaborate with you on something new, and you must also be aware of dot.

1 Like

Thanks for making me discover flo_draw, as well as its companion crates. This looks promising.

Yes, but this crate only create the graphviz text file. It doesn’t do the rendering. In my case it was easier to create that file by hand than to rely on proc macro. I would have been interested by a crate that makes the rendering using graphviz, instead of relying on an external command.

@ahunter AFAICT you don’t have anything (yet) to draw colored text in flo_* or did I miss something?

I've added support for text just this week: you'll need to get the v0.3 branch from github as it's not quite ready to be published yet. There's a basic Hello, world example to look at to show how it works.

I'm probably going to make a few changes to the API before releasing this, though changing over shouldn't be too hard once that happens.

1 Like

I don’t understand why adding flo_draw = { git = "ssh://git@github.com/Logicalshift/flo_draw.git", branch = "v0.3" } in the dependencies section of my Cargo.toml doesn’t works. I get this error:

error: failed to select a version for the requirement `flo_canvas = "^0.3"`
candidate versions found which didn't match: 0.2.0, 0.1.0
location searched: crates.io index
required by package `flo_draw v0.3.0 (ssh://git@github.com/Logicalshift/flo_draw.git?branch=v0.3#71d166a0)`
    ... which is depended on by `cargo-callgraph-flo v0.1.0 (/Users/robinm/perso/cargo-callgraph-flo)`

In the meantime, I’ve downloaded a local copy of flo_draw, and I’m editing the example in-place.

@ahunter A few question:

  • Why does draw_text() takes a String and not an &str? It feels wasteful to require an allocation just for drawing it.
  • Can I access the bbox enclosing the generated text? To be more precise, I need to be able to draw a single line of text with many colors, then create an underline below some words, and finally I will have arrows that start from the middle of that underline, or point to the middle of the section above the text.

As you can see, I’m switching colors quite a lot. The code will looks more or less like:

// first draw the text

gc.fill_color(/* black */)
gc.draw_text(FontId(1), "fn".to_string(), baseline_x, baseline_y);

gc.fill_color(/* orange */)
gc.draw_text(FontId(1), "foo".to_string(), baseline_x + width_of("fn "), baseline_y);

gc.fill_color(/* black */)
gc.draw_text(FontId(1), "::".to_string(), baseline_x + width_of("fn foo"), baseline_y);

// ...

// then draw the underline

gc.new_path();
gc.move_to(baseline_x + width_of("fn foo::bar::"), baseline_y + height_of_text() + 5.0);
gc.line_to(baseline_x + width_of("baz"), baseline_y + height_of_text() + 5.0);
gc.line_width(3.0);
gc.stroke_color(/* green */);
gc.stroke();

I need to access to the width of the various part of the text (width_of()), as well as its height (height_of_text()). I don’t think I will use multiple fonts, but I may want to mix bold and non-bold text. How can I do that?

That's because it's going to be queued as a task for the renderer to do, which would need to copy a &str but can just move a String, so the idea at the moment is to leave it up to the user to decide how the copy is made. That's not set in stone, and copying the string is rather a small amount of work compared to the typesetting that's going to happen later on so I'm open to change this at the moment.

It wasn't possible before as the state was all hidden, but the latest version provides a measure_text() routine and even a general purpose font layout struct that can probably do what you want. There's a new example demonstrating some of the layout effects that can be achieved.

You can now use measure_text() to read the size of the various parts of the string and use that to format them manually, as you described.

You could also use the new CanvasFontLineLayout structure to format everything at once: measure() will tell you the current bounds of the text so can be used to find coordinates for your arrows, and draw() is useful for both changing the colour of the text mid-way through and adding annotations like the arrows. continue_with_new_font() is how you'd switch to a bold typeface, and to_drawing() will give you a list of instructions to send to gc.draw_list() (you can use a transform to move the text wherever you need as it'll be typeset starting at 0,0)

1 Like

@ahunter

I finally took the time to look at your new example and the measure_text() method. Thanks a lot for providing such good examples, it’s really great.

I tried to look at the implementation, but my knowledge of how fonts/glyph/rasterization/… is way too limited to be useful. I would have preferred way more to send patches, that just asking for more features, so I hope you don’t mind.

I realized that I gave you a bad example. Drawing the text, then computing again the size of the glyphs that where just rendered (ie. using measure_text() with a similar &str that what was just drown), while it works for my use-case, probably doesn’t work in general because of right-to-left languages, ligatures, … and it’s harder to use. I think that ideally the function draw_text_layout() should return the TextLayoutMetrics which has already access to the text, font, and its size. Of course that TextLayoutMetrics would generally be ignored, unless the user wants to do something fancy like drawing a box around the text that was just drawn.

EDIT: I realized that layout_text() should also returns a TextLayoutMetrics, allowing for easier measurements (so you don’t have to repeat the font + font-size). Likewise the return value would be discarded most of the time. And if TextLayoutMetrics implements std::ops::Add and std::ops::AddAssign it becomes really easy to access to the size of multiples combined glyphs.

This links has a nice schema that will help defining some terms. I’m not sure what the y parameters in begin_line_layout() is exactly. Ideally it should be the baseline (so changing the font would not change the layout). If I understand correctly measure_text() returns two points that defines the blackbox height and width. If the y was effectively the baseline, then we can deduce the ascender and descender. I think that it’s important to have access to all of those metrics.

I have no idea if the underline position and thickness is defined by the font + font size, or if it’s completely arbitrary. A quick search only gave me results on how to set the underline position in CSS :wink: If the default position of the underline is set by the font and its size, I think it would be reasonable to implement it in flo directly (ideally with a color that could be different than the one of the text). Otherwise, I think it’s totally reasonable to let the user draws it (especially if it has access to the baseline and the descender).

With the line layout, the meaning of the coordinates are defined by the alignment, although all of the current alignments use it as a baseline.

The key to doing layout the way you want is the CanvasFontLineLayout struct. I've done some more work recently to make things a bit more ergonomic. There's now a font_metrics function that takes care of adjusting the units for your font size: this does include information on where to draw the underline.

I've also updated the text_layout example to show 'fully manual' text layout using the new features: this makes it possible to achieve effects like I think you're looking for. One new thing is a pos field in the layout metrics: that indicates where the next glyph will be placed.

The usual graphics routines are sending commands to a remote thread, so they don't have any way to provide feedback about things like layout, which is why the CanvasFontLineLayout struct exists: it can perform the layout, supply feedback as it goes and tell the downstream renderer to produce exactly that layout.

Here's how the example looks:

and the code to generate it:

let lato_metrics    = lato.font_metrics(18.0).unwrap();
let mut text_layout = CanvasFontLineLayout::new(&lato, 18.0);

text_layout.add_text("Manual layout allows ");

let start_pos   = text_layout.measure();
text_layout.add_text("custom");
let end_pos     = text_layout.measure();

text_layout.new_path();
text_layout.move_to(start_pos.pos.x() as _, start_pos.pos.y() as f32 + lato_metrics.underline_position.unwrap().offset);
text_layout.line_to(end_pos.pos.x() as _, end_pos.pos.y() as f32 + lato_metrics.underline_position.unwrap().offset);
text_layout.stroke_color(Color::Rgba(0.8, 0.6, 0.0, 1.0));
text_layout.line_width(lato_metrics.underline_position.unwrap().thickness);
text_layout.stroke();

let mid_point = (start_pos.pos + end_pos.pos) * 0.5;
text_layout.move_to(mid_point.x() as _, mid_point.y() as f32 + lato_metrics.underline_position.unwrap().offset);
text_layout.line_to(mid_point.x() as _, mid_point.y() as f32 + lato_metrics.underline_position.unwrap().offset - 8.0);
text_layout.stroke_color(Color::Rgba(0.8, 0.6, 0.0, 1.0));
text_layout.line_width(2.0);
text_layout.stroke();

text_layout.begin_line_layout(mid_point.x() as _, mid_point.y() as f32-lato_metrics.underline_position.unwrap().offset - 30.0, TextAlignment::Center);
text_layout.layout_text(FontId(1), "here".to_string());
text_layout.draw_text_layout();

text_layout.add_text(" drawing effects, such as this underline");

text_layout.align_transform(500.0, 370.0, TextAlignment::Center);
gc.draw_list(text_layout.to_drawing(FontId(1)));

The call to align_transform() determines how the coordinates relative to the layout translate to the canvas. If you use TextAlignment::Left you just need to add the x and y coordinates to the values returned by measure(). Speaking of measure(), it interrupts the layout, which takes care of things like right-to-left text, ligatures, kerning and so on by fixing the layout at that point: it's best used at word breaks or punctuation points for that reason (there is definitely more work needed for full right-to-left support right now, but I think this will remain the case).

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.