Core Graphics
Draw diagrams with Core Graphics on Apple's platforms, and measure their text with Core Text.
The limu-coregraphics crate draws limu’s scenes with Core Graphics, into a CGContext your app gives it or into a CGImage it makes. Text is shaped by Core Text, as the rest of an app’s text is on Apple’s platforms, so a diagram uses the system’s fonts or the fonts your app carries. It suits apps on macOS and iOS that draw in views of their own, through AppKit, UIKit or another toolkit that hands out a CGContext.
limu-rs is young, and its API will change in any 0.x release.
Installing
limu-coregraphics is a crate of its own, which keeps limu and limu-scene free of anything beyond Rust’s standard library. Add both crates.
1cargo add limu limu-coregraphicsThe crate calls Core Graphics and Core Text through objc2’s bindings, and re-exports them as limu_coregraphics::objc2_core_foundation, objc2_core_graphics and objc2_core_text. It is empty on platforms other than Apple’s, so a project that builds for other platforms too can depend on it everywhere. The examples below use limu::scene, which is the limu-scene crate as limu re-exports it.
Drawing an image
limu_coregraphics::render draws a scene into a new image. Lay out the diagram with the same fonts you draw it in, as explained under Fonts.
1use limu::scene::style::{Look, Theme};2use limu_coregraphics::{Fonts, Options};34let fonts = Fonts::from_data(&std::fs::read("Inter-Regular.ttf")?)?;5let layout = limu::layout::Options {6 measurer: &fonts,7 ..Default::default()8};9let scene = limu::layout(src, &layout)?;10let o = Options {11 look: Look::new(Theme::KOHU),12 fonts: Some(&fonts),13 scale: 2.0,14 ..Options::default()15};16let image = limu_coregraphics::render(&scene, &o).ok_or("the diagram is too large to draw")?;The image is in sRGB, as large as the scene times Options.scale, rounded up to whole pixels, and filled with the theme’s background. A scale of zero or less counts as 1. render returns None when the scene is too small or too large for an image. Show the image in a view or a layer, or write it to a file with Image I/O.
Options.look sets the theme and the overrides. See Theming. When the look changes, for example when the system switches to dark mode, draw the scene again. It doesn’t need to be laid out again.
Drawing in a view
limu_coregraphics::draw draws a scene into a context you give it, in the context’s user space, with y running down. A flipped view’s context is set up this way already, so in a flipped NSView, or in a UIView, draw straight into the context of draw(_:). Scale the context first to zoom.
1use limu_coregraphics::objc2_core_graphics::CGContext;23/// Draws a scene in a view's context, zoomed. A flipped view's context is y down, as4/// the scene is.5fn draw_zoomed(cx: &CGContext, scene: &Scene, o: &Options<'_>, zoom: f64) {6 CGContext::save_g_state(Some(cx));7 CGContext::scale_ctm(Some(cx), zoom, zoom);8 limu_coregraphics::draw(scene, o, cx);9 CGContext::restore_g_state(Some(cx));10}A context whose y runs up, such as a bitmap context’s or an unflipped view’s, needs to be turned over first, as render does with its own.
1// A context whose y runs up, height units high, is turned over first.2CGContext::translate_ctm(Some(cx), 0.0, height);3CGContext::scale_ctm(Some(cx), 1.0, -1.0);draw doesn’t fill a background, and it ignores Options.scale. It leaves the context’s state as it found it.
limu-coregraphics draws the same picture as the SVG writer, with one difference. When a corner radius is too large for a box, it uses the smaller of half the width and half the height. SVG uses elliptical corners instead.
As in SVG, an item’s opacity is drawn as a transparency layer, so where an item’s parts overlap, the lower part doesn’t show through.
Fonts
A diagram is laid out with a measurer and drawn with fonts. If the two don’t match, text won’t fit the space layout gave it. limu_coregraphics::Fonts does both jobs. It implements limu’s Measurer, and it measures each line as Core Text sets it for drawing.
Fonts::from_data takes the data of a TrueType or OpenType file as the regular font, for an app that carries its fonts, and set_data adds a font for a face. They return a FontError for data that isn’t a font. Fonts::new and set take a CTFont instead, for the system’s fonts. The faces are bold, italic and mono, and any mix of them.
1use limu::scene::text::Face;2use limu_coregraphics::Fonts;3use limu_coregraphics::objc2_core_foundation::{CFRetained, CFString};4use limu_coregraphics::objc2_core_text::CTFont;56fn font(name: &str) -> CFRetained<CTFont> {7 // SAFETY: a null matrix stands for the identity.8 unsafe { CTFont::with_name(&CFString::from_str(name), 12.0, std::ptr::null()) }9}1011let mut fonts = Fonts::new(font("Helvetica"));12fonts13 .set(Face::BOLD, font("Helvetica-Bold"))14 .set(Face::ITALIC, font("Helvetica-Oblique"))15 .set(Face::MONO, font("Menlo-Regular"));The size a font was made at doesn’t matter, because text is set at the size its label asks for. Text in a face you gave no font for is set in the nearest one you did. limu-coregraphics tries the face without italic, then without bold, and then takes the regular font. Without Options.fonts, text is left out. If you don’t give layout a measurer, it measures text as Arial, so a diagram drawn in another font only fits when that font has Arial’s widths.
Core Text falls back to the system’s fonts for characters the font doesn’t have, such as Chinese, Japanese or Korean in a Latin font. Fonts measures the line with the fallback fonts in it, so that text fits too. Glyphs are drawn from their outlines, so a halo is stroked the way any path is. An underline follows the font’s own position and thickness. Subscripts are lowered a fifth of the label’s size and superscripts raised a third, about where a browser puts them.
Icons and images
limu-coregraphics never loads images or follows links on its own. Your app knows where its images come from, so it makes those decisions.
Options.icons gives an icon’s parts by its name, such as fa:fa-car. Every backend of limu-rs takes icons in this one form, limu::scene::Icons, so the icon set written for tiny-skia works here as it is. The parts sit on a box of 24 by 24. A filled part is filled in the color of the item’s text, and a stroked part is stroked in it, 2 wide with round ends and corners. Layout leaves room in labels only for icons you report through layout::Options::icons, so give both the same set.
Options.images gives a CGImage for an image’s URL. When an image’s fit is set, it keeps its proportions and is centered in its box. Otherwise it’s stretched to fill the box. Layout only passes through URLs whose scheme layout::Options::image_schemes allows, and loading and caching the images is up to you. Without icons or images, icons and images are left out.
1let o = Options {2 fonts: Some(&fonts),3 icons: Some(Box::new(icon)),4 // images is a HashMap<String, CFRetained<CGImage>> of the images loaded already.5 images: Some(Box::new(|src| images.get(src).cloned())),6 ..Options::default()7};When an item with a link is clicked, its link field holds the target. Open it the same way your app opens any other link, or don’t. Scene::item_at finds the item at a point in the scene’s units, as described on the GPUI page.
Custom drawing
limu_coregraphics::draw_with draws each item with a hook in place of the default. The hook receives a Painter, the item and its style as a Resolved, with every color already worked out. The painter’s item draws the item the default way, and parts, ends, picture and label draw the default’s pieces. Its canvas is a ContextCanvas, whose context lets the hook draw with Core Graphics directly, in the scene’s units.
1use limu::scene::Kind;2use limu_coregraphics::objc2_core_foundation::{CGPoint, CGRect, CGSize};3use limu_coregraphics::objc2_core_graphics::CGContext;45limu_coregraphics::draw_with(&scene, &o, cx, |p, it, s| {6 if !matches!(it.kind, Kind::Node) {7 p.item(it, s);8 return;9 }10 // Draw the node as a box of our own with square corners, and its label the default way.11 let cx = Some(p.canvas().context());12 let (b, c) = (it.bounds, s.fill.color);13 let k = |v: u8| f64::from(v) / 255.0;14 CGContext::set_rgb_fill_color(cx, k(c.r), k(c.g), k(c.b), k(c.a));15 CGContext::fill_rect(cx, CGRect::new(CGPoint::new(b.x, b.y), CGSize::new(b.w, b.h)));16 p.label(it, s);17});The hook draws inside the transparency layer of the item’s opacity, so the style it receives is always opaque.