Read code of egui

What is this?

I will read the code ofegui, and I will describe how it works. egui is GUI library for Rust.

This is created with almost full scratch. This is great and this is useful for understanding how the GUI works.

After understanding code, I will try to implement simple version of egui in Rust. If you interest for creating the GUI library, let's try it.

Overview of how the GUI work

The GUI like browser works like the following.

  1. Construct the UI tree consists of user input(e.g. HTML/CSS)
  2. Construct the layout tree
  3. Construct the paint tree
  4. Defines the layer order
  5. Paint

In real world, the following type of framework is used as the GUI as far as I know.(I wonder if we can call these a "framework"...)

  • Chrome(browser) ... Parse HTML/CSS/JavaScript then create the DOM and style tree, then construct the layout tree and paint.
  • egui ... Construct layout tree in Rust, and paint.
  • Tauri... Pass HTML/CSS/JavaScript to the webview and the webview draw the rect.

In egui works withimmediate mode. The GUI has two strategy like the following.

  • Immediate mode
  • Retained mode

The immediate mode construct layout every frame. On the other hand, the retained mode construct layout when the stored layout get changed.

See more detail of immediate mode inthe documentation of egui.

Read example of egui

First, we need to known how egui works. So let's try to see example in egui.

I tried to runhello_world example. This is so simple example.

The entry point is the bellow in example.

fn main() {
let options = eframe::NativeOptions::default();
eframe::run_native(
"My egui App",
options,
Box::new(|_cc| Box::new(MyApp::default())),
);
}

eframe::run_nativeis method for runningMyAppon native window. egui can run app on everywhere like native, browser.

eframe::run_nativealso has role for painting the constructed UI. We will seeeframe::run_nativelater.

And scrolling down, then theMyAppstruct is defined.

struct MyApp {
name: String,
age: u32,
}

// ...

impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My egui Application");
ui.horizontal(|ui| {
ui.label("Your name: ");
ui.text_edit_singleline(&mut self.name);
});
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
if ui.button("Click each year").clicked() {
self.age += 1;
}
ui.label(format!("Hello '{}', age {}", self.name, self.age));
});
}
}

This code define the implementation ofMyAppfor satisfyingeframe::App. TheMyApp::updatemethod is called each time that the UI needs repainting.

InMyApp::updatemethod,egui::CentralPanel::default().show()method is called. egui has somePanelthat includesegui::CentralPanel. the followingPanelis defined.

  • CentralPanel
  • SidePanel
  • TopBottomPanel

You can seethe documentation about panelfor more detail, but theCentralPanelmust to be used in app. And these panels indicate region of UI. TheCentralPanelindicates main column.

Theegui::CentralPanel::default().show()defines the following element.

  • Define panel's layer
  • Compute layout
  • Prepare painting

And App paint the UI after processing the above steps ineframe::run_native.

Read the detail of the code.

Layout

We could learn the overview of egui works, so we can dive into the code more deeply.

First, I will read the detail ofegui::CentralPanel::default().show()method.

Theshowmethod is defined in/crates/egui/src/containers/panel.rs. This method callsshow_dyninternally which is defined inthe same file. This method perform the following processes.

  • Calculate available rect of background layer.
  • Set layer order as background
    • Layer order is define inhere.
  • Set id for UI.
  • Decide the region of this panel.
    • This is relative position to previous element.
  • Invokeself.show_inside_dynmethod.
  • Callctx.frame_state().allocate_central_panel(inner_response.response.rect);.

Theself.show_inside_dynmethod construct UI in the following steps.

  • Calculate available rect from cursor. The cursor indicates current rect in frame(window) inhere.
  • Invokeframe.show()method.

Theframe.show()method is defined inhere. In this method, finally UI's layout is calculated inframe.show_dyn()method which is called insideframe.show()method.

Inself.begin()method which is called inframe.show(), this calculating layout of panel like margin. Andadd_contents()closure is called to calculate UI layout. This was passed fromApp::update()method that is entry point to render UI. I will describe the detail ofadd_contents()later.

Finallyself.end()method is called. This method perform the following things.

We could know the overall of the layout, so let's we discuss the detail ofadd_contents()that we just skipped.

Fist, here we see theui.horizontal()method which is defined in theadd_contents().ui.horizontal()is definedhere. Andself.horizontal_with_main_wrap_dyn()method is called, this is definedhere. In here, layout process is similar to the frame layout process we just discussed, but layout direction is definedright-to-leftorleft-to-right.

Next, let's see theui.label()which is called in closure passed toui.horizontal(). Theui.label()is defined inhere, callLabel::ui()method which is defined inhere. InLabel::uimethod,Label::layout_in_uiis called. This processes the following things.

  • Check if widget is galley, if so, then skipping the text layout process.
    • Galley means text which has already been calculated the layout process.
  • Constructtext_job.
  • Wrap the rect or text inhere.
    • If all of rect should be wrapped, first condition is used.
    • In other cases, else condition is used.
    • By callingtext_job.into_galley(), text is layed out and cached.
    • Let me describe about text layout later.

Finally, painter forLabelis prepared. That is, the layout ofLabelis finished.

Let me skip describing aboutui.text_edit_singleline(&mut self.name);inui.horizontal(), because this is little complex.

BTW how do events work like click? Let me describe these work. Event is handled by window manager likewinit. Thewinithandle some event through OS specific function. For example, in macOS, the winit is using the Rust binding for objective-c to handle macOS event by observing event which is sent fromNSApplication. When the UI receive the event from the window manager then the UI changes the state that context has, then the UI is repainted. In this time, for example, if click event is received from the window manager, click event has a clicked position on the window but the UI don't know which widget is clicked, so in UI painter calculate whether click position is within rect of widget. In egui, theApp::updateis called when event is sent, and the UI's state is changed, for exampleUI.button().clicked()will betrue.

Paint

Next, we will discuss about the painting flow. In egui,eframe::run_native()method is used for painting for native app.eframe::run_native()is defined inhere, this will select the painter. In egui, the following painters are used for each target.

  • glow... This is used as painting backend. This works everywhere as wasm.
  • wgpu... This is used as painting backend and added for compatibility described inAdd egui_wgpu crate. This works everywhere as wasm.
  • glium... Previously, the glium is used for painter backend, but currently, this is remained for compatibility. (Replace Glium issue)

In this article, we will read the code of the glowdefined in herefor native application.

Let's take a entry point of the glow code. The entry point is defined inhere.

Read the code, then we can find the event loop is created bywith_event_loop. Inwith_event_loop, event loop is created in another thread by using winit.

After creating event loop, event loop is run inrun_and_returnfunction. In event loop inrun_and_returnfunction, firstwinit_app.on_event()method is calledin here. Thewinit_appindicatesGlowWinitAppin case using the glow.

Inwinit_app.on_eventmethod, it handle some events, and theGlowWinitAppneed to setupself.runninginself.init_run_state()which is defined inhere. In this method, the openGL is first prepared by invokingSelf::create_glutin_windowed_context()andcreatingthePainter. Second, theEpiIntegrationis created inhere. It is usingEipIntegrationfor handling winit, openGL, and more for drawing on window. Finally,self.runningis set.

IfEvent::RequestRedrawis send fromherein each time depends onnext_repaint_time,Event::RequestRedrawis matched andwinit_app.paint()is invoked inthis block.

winit_app.paint()for glow is defined inhere.

Inwinit_app.paint()method, first, it clear display by clear color.

Andintegration.update()method is called. This method is define inhere. In this method, the frame rect is first prepared.read_window_info()function get window information from winit window like position and size. Alsoself.egui_winit.take_egui_input()method gettheRawInputstruct. TheRawInputindicates users input likeevent,modifiers.

Next,self.egui_ctx.runis called inthis line, and this method is defined inhere.

In this method, first,begin_frame_mutmethodis called. Thebegin_frame_mut()method prepares the following properties inContextImplstruct.

  • memory...ContextImpl's memory persists like the following values.
    • event... This is state for event like click, drag, cursor move, focus, etc. And memory stores the state which event was fired.
    • area... This indicates the layer order. And interactable area is defined.
    • fonts... This is formal information of font. Calculated font information is cached.
  • input... This indicates amount of scroll, amount of zoom, which key is pressed, touch and pointer position. Also it defines framerate byunstable_dt,stable_dt,predicted_dt.
  • frame_state... This stores current frame state withinput.

Inself.egui_ctx.run()method, additionally,run_ui()is called. This construct UI byApp::update()method which we defined in entry file.

Finally,end_framemethodis called. In this method, first, it checks whether repainting is needed byself.input().wants_repaint(). It checks whether pointer is moved or window is scrolled, etc. Next,font shaping is calculated. Also,PlatformOutputisgenerated fromContext::output. ThePlatformOutputis used for platform specific work like the screen reader, copying copied text to clipboard, opening url. And the final work of theend_framemethod is to take the paint list. The paint list is stacked in layout process through thegraphicsofContext. In the layout process, the UI struct is created in each time. Alsothe UI struct haspainter, and the painter hasContextinside this struct. ThegraphicofContexthas a internal mutability throughArcandRwLock, so thegraphicof Context can be constructed through thepainterofUIstruct.

Let's go back to theintegration.update()which we just read. We read up toself.egui_ctx.run()inside theintegration.update(). Next,newfull_outputis made be pending, and olderfull_outputis taken. Finally, handling some window configuration finished, then olderfull_outputis returned.

Well, let's go back to the caller ofintegration.update(). We have read up to [integration.update()] method insideGlowWinitApp::paint()method. Next,integration.handle_platform_output()method is called. This handles the following things.

Next,integration.egui_ctx.tessellate()is invoked. This is defined inhere. This convertClippedShapeintoClippedPrimitive.ClippedShapeis created in the layout phase. Thetessellationmeans construct rect by triangle.eguiis usingOpenGL.OpenGLis drawing the element by triangle, so it needs to draw the element by using triangle. That is, to draw an element by triangle, tessellation is used.

After tessellating,painter.paint_and_update_textures()is called. Inpainter.paint_and_update_textures(), rect is drawn bythedraw_elementsmethod of the OpenGL. Thenswap_buffers()is called for swapping double buffering.

Finally, the rect is displayed on the window!

Conclusion

egui is really great project. It is implemented all of features for drawing by full scratch. Currently, egui is also only supportingwgpufor drawing with low level API. It will be used for more performant case. To learn more painting system, I think I will readthe webrenderorthe skia.