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.
- Construct the UI tree consists of user input(e.g. HTML/CSS)
- Construct the layout tree
- Construct the paint tree
- Defines the layer order
- 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_native
is method for runningMyApp
on native window. egui can run app on everywhere like native, browser.
eframe::run_native
also has role for painting the constructed UI. We will seeeframe::run_native
later.
And scrolling down, then theMyApp
struct 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 ofMyApp
for satisfyingeframe::App
. TheMyApp::update
method is called each time that the UI needs repainting.
InMyApp::update
method,egui::CentralPanel::default().show()
method is called. egui has somePanel
that includesegui::CentralPanel
. the followingPanel
is defined.
CentralPanel
SidePanel
TopBottomPanel
You can seethe documentation about panelfor more detail, but theCentralPanel
must to be used in app. And these panels indicate region of UI. TheCentralPanel
indicates 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.
Theshow
method is defined in/crates/egui/src/containers/panel.rs
. This method callsshow_dyn
internally 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.
- Invoke
self.show_inside_dyn
method. - Call
ctx.frame_state().allocate_central_panel(inner_response.response.rect);
.
Theself.show_inside_dyn
method construct UI in the following steps.
- Calculate available rect from cursor. The cursor indicates current rect in frame(window) inhere.
- Invoke
frame.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.
- Calculate background rect by calling
self.paint_rect()
.- This calculate how range should be painted.
- This expand rect includes
frame.inner_margin
(This is the padding of browser.). - Invoke
frame.paint()
method to define shape of rect which is defined inhere. - Call
self.allocate_rect()
to movecursor
and handle interaction like hover.- Expand
cursor
includes frame rect and spacer by invokingself.advance_cursor_after_rect()
method. - Check frame rect is hovered in
self.interact_with_hovered
.
- Expand
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-left
orleft-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::ui
method,Label::layout_in_ui
is 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.
- Construct
text_job
.text_job
is consist of font layout and text style by callinginto_text_job
.- Default text style is defined in here.
- 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 calling
text_job.into_galley()
, text is layed out and cached. - Let me describe about text layout later.
Finally, painter forLabel
is prepared. That is, the layout ofLabel
is 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. Thewinit
handle 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::update
is 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_return
function. In event loop inrun_and_return
function, firstwinit_app.on_event()
method is calledin here. Thewinit_app
indicatesGlowWinitApp
in case using the glow.
Inwinit_app.on_event
method, it handle some events, and theGlowWinitApp
need to setupself.running
inself.init_run_state()which is defined inhere. In this method, the openGL is first prepared by invokingSelf::create_glutin_windowed_context()
andcreatingthePainter. Second, theEpiIntegration
is created inhere. It is usingEipIntegration
for handling winit, openGL, and more for drawing on window. Finally,self.running
is set.
IfEvent::RequestRedraw
is send fromherein each time depends onnext_repaint_time
,Event::RequestRedraw
is 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 gettheRawInput
struct. TheRawInput
indicates users input likeevent
,modifiers
.
Next,self.egui_ctx.run
is called inthis line, and this method is defined inhere.
In this method, first,begin_frame_mut
methodis called. Thebegin_frame_mut()
method prepares the following properties inContextImpl
struct.
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_frame
methodis 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,PlatformOutput
isgenerated fromContext::output
. ThePlatformOutput
is used for platform specific work like the screen reader, copying copied text to clipboard, opening url. And the final work of theend_frame
method is to take the paint list. The paint list is stacked in layout process through thegraphics
ofContext
. In the layout process, the UI struct is created in each time. Alsothe UI struct haspainter
, and the painter hasContext
inside this struct. Thegraphic
ofContext
has a internal mutability throughArc
andRwLock
, so thegraphic
of Context can be constructed through thepainter
ofUI
struct.
Let's go back to theintegration.update()
which we just read. We read up toself.egui_ctx.run()
inside theintegration.update()
. Next,newfull_output
is made be pending, and olderfull_output
is taken. Finally, handling some window configuration finished, then olderfull_output
is 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.
- screen reader support
- In
platform_output.events_description()
method, this generates description which includes event name, widget name, and contained text label for screen reader.
- In
- set cursor icon
- open URL with available browser
- set copied text to clipboard
- set IME position
Next,integration.egui_ctx.tessellate()
is invoked. This is defined inhere. This convertClippedShape
intoClippedPrimitive
.ClippedShape
is created in the layout phase. Thetessellation
means construct rect by triangle.egui
is usingOpenGL
.OpenGL
is 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_elements
method 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.