Skip to content

Proposal: Split widgets from the core library. #8277

Description

@michalsustr

Thesis

Egui can become more powerful and themeable if it separated widgets/components from the core library.

I'd like to propose a new approach egui could adopt. It is fairly radical, but I think it is radical in the right direction: it reduces the amount of configuration egui needs to carry internally, while making the library more customizable.

I will first motivate an example, then explain the concrete split, then explain why I think this is the right library design approach for egui.

Disclaimer: I've used LLM to pass through and polish the writing.

Motivation

Right now, egui makes the application code beautifully simple:

if ui.button("Delete").clicked() {
    delete_item();
}

This is one of egui's greatest strengths.

Inside that single call, several different concepts are fused together:

  • layout
  • hit-testing
  • pointer/keyboard interaction
  • disabled/hover/pressed state
  • accessibility semantics
  • text measurement
  • painting

That coupling is ergonomic for users, but inconvenient for systematic modifications like UI styling.

Consider the following use-case: I would like to make a skeuomorphic design theme, for example like Windows XP. This involves styling many components that should be rendered consistently everywhere. Like a scrollbar:

Image

That sounds like a styling or a theme change. But in practice, implementing this is not merely a theme in the narrow sense of “change the color from gray to blue.” It is also not merely a matter of adding more fields to egui::Style. A Windows XP-style scrollbar has a particular visual grammar, like:

  • Non-overlay scrollbar gutter,
  • Always visible track, or at least layout-reserving track,
  • Raised and sunken 3D bevels,
  • Separate arrow buttons at the ends,
  • Different visual states for normal, hovered, pressed, disabled,
  • Possibly a dotted or gripped center region.

It is quite complex, you get the idea.

But the semantics of the scrollbar are not unusual. It should still scroll the same way, the mouse wheel should work, scroll_to APIs should still work, accessibility should still describe the same thing, and so on.

Egui already has useful style knobs, and proposals like CSS-like styling and per-component styles are useful directions. However doing this with style overrides is not really possible unless we emulate the full richness of CSS, or some equivalent styling language. And this would not come for free. We would pay for it with:

  • More configuration structs,
  • More fields that every component needs to check,
  • More branching in hot UI code,
  • More memory usage for style data,
  • More invalid states,
  • More documentation burden,
  • More coupling between components and styling,
  • And a styling language that is still less expressive than Rust.

The more expressive the style system becomes, the more it starts to look like a second programming language. But Rust is already a programming language. It is fast, statically checked, optimized by the compiler, and already the language egui users are writing.

So instead of teaching a style struct how to describe every possible scrollbar, I think egui should expose a seam where users can replace the scrollbar implementation itself.

The mind shift one needs to take is that A scrollbar is not a colored rectangle. It is a little program.

This is the case with every other UI components. Trying to describe all of this through a style struct is backwards. You eventually reinvent a slower, incomplete, worse programming language.

Forking, but with a boundary

So what can one do? People who are serious about egui styling already tend to do one of two things: they fork egui, or they reimplement large sets of UI components from scratch.

That sounds bad, because forking usually means you slowly lose contact with upstream. But I do not think the problem is that downstream experimentation exists. The problem is that most libraries are not designed to make downstream experimentation cheap.

Right now, if I want to change a deeply integrated component such as a scrollbar, I have to touch too much. I do not only want to replace a color. I want to replace a small behavior/rendering program while preserving egui's core semantics.

So the goal should not be “make every possible component configurable.”

The goal should be:

Keep the stable semantic substrate upstream, but make the default component implementations replaceable.

This is a much smaller and cleaner problem.

Proposed split

For egui, the natural split could be:

egui-core
egui-components
egui

Where:

egui-core

contains the substrate (what is now egui):

  • input
  • ids
  • responses
  • layout
  • rects
  • shapes
  • text rendering
  • painting/interaction primitives
  • accessibility hooks
  • state machinery
  • memory
  • etc.

And:

egui-components

contains the particular UI components:

  • button
  • checkbox
  • slider
  • scrollbar
  • text edit
  • window
  • menu
  • combo box
  • panels
  • etc.

Concretely, this is what should be split away from egui-core:

  • component modules (button, checkbox, text edit, image, slider, etc.)
  • containers/* (window, scroll area, menu, combo box, panels, popups, etc.)
  • grid
  • deprecated menu/popup compatibility helpers
  • Ui convenience methods that construct concrete components (ui.button, ui.checkbox, ui.menu_button, etc.), moved to extension traits in egui-components

And:

egui

is the compatibility/default facade that gives most users the normal egui experience.

That facade can re-export egui-core and egui-components, so ordinary users can still depend on egui and get the normal batteries-included library.

But the core crate would no longer have component shortcuts like:

ui.button("Delete")

Instead, they belong to the default component pack.

The default experience will be provided by an extension trait:

use egui_components::prelude::*;

if ui.button("Delete").clicked() {
    delete_item();
}

Or the egui facade crate could import/re-export the default component prelude so that normal applications barely notice the split.

But at the architectural level, the important thing is that there is a substrate-only egui where Ui does not know about button.

Ui should know how to allocate space, assign ids, handle input, produce responses, paint shapes, and maintain state. It should not necessarily know what a button looks like.

Where does styling fit in?

This is necessarily not an argument against styling.

Styling is good for ordinary changes, like colors, spacing, rounding, etc. But some changes are not parameter changes. They are implementation changes -- like the scrollbar example I gave. For complex changes like that, the honest representation of the component is code.

This is especially true in egui, because components are already small immediate-mode programs. A component is not a persistent object tree waiting to be styled later. It is code that runs, allocates, interacts, and paints.

So I think the clean design is:

  • Use style structs for simple parameterization.
  • Use replaceable component code for structural changes.

This reduces complexity, because egui does not need to anticipate every possible future theme. It does not need a giant universal style schema. It does not need to add another option every time someone wants a slightly different button, scrollbar, menu, or slider.

Performance

I think this is also a performance win.

A very expressive styling system is not free. Even if each individual option is cheap, a large style system tends to create costs everywhere:

  • more style data to store,
  • more options to copy or borrow,
  • more branching in components,
  • more cache pressure,
  • more invalid combinations,
  • more code paths,
  • more testing,
  • more “what does this field mean for this component?” complexity.

And the frustrating part is that even after paying these costs, the style system still will not be expressive enough for all serious customization.

What stays stable

The stable layer should define the contracts custom components must preserve:

  • how ids are generated and used,
  • how layout is allocated,
  • how Response works,
  • how focus works,
  • how accessibility metadata is attached,
  • how scrolling state is represented,
  • how pointer and keyboard interaction should behave,
  • which invariants components must maintain,
  • which pieces of state are semantic rather than visual.

The replaceable layer is the rendering implementation of particular components.

Why this helps third-party components

A split like this also gives the ecosystem a better customization boundary.

Today, if a third-party component internally uses egui's built-in components, the application's theme has only limited control over those internals. If the component uses a scroll area, text edit, combo box, or menu, it tends to inherit egui's default implementation, and only the styling changes can be applied.

With a replaceable component pack, third-party crates can intentionally depend on the component abstraction layer instead of hard-coding a particular visual implementation.

This would not automatically fix all third-party components. Code that implements its own custom scrollbar will still do its own thing. But it gives the ecosystem a clear convention:

Depend on the core semantics and call through the component pack if you want your component to be theme-replaceable.

Then if I want my application to look like Windows XP, I should not need to fork all of egui.

I could fork or replace the component pack:

[patch.crates-io]
egui-components = { path = "../egui-components-winxp" }

Then all components that go through that component pack get the same visual grammar.

Fragmentation

One obvious concern is fragmentation. If components are easier to replace, will the ecosystem split into incompatible egui-like libraries? I think the opposite is more likely.

Today, serious users of egui make their own customization. This pushes people toward forking the whole library or reimplementing whole component sets from scratch. That is existing fragmentation, without a clean boundary.

A core/components split makes the boundary explicit.

It lets experimental component packs exist without forking input handling, layout, painting, accessibility, ids, responses, and all the other hard substrate work.

It also makes upstream egui stronger, because the core becomes the stable shared foundation.

The goal is not to encourage permanent forks. The goal is to make downstream experimentation possible without forking the entire GUI substrate.

Why now?

Historically, this kind of split would have been expensive.

If every user had to understand the internals of egui deeply, then a replaceable-component architecture would be too hard. It would create a lot of burden for both maintainers and users.

But the situation has changed.

Cheap LLMs make library code much easier to inspect, fork, adapt, and keep close to upstream. They are not a replacement for good architecture, but they change the cost model of customization. A design that exposes clear seams is much more valuable now, because users have better tools for working across those seams.

Once you have a well-designed component library that serves as a template for customization, with good reuse of its building blocks, you can point LLM to the library and just tell it what kind of theme customization you'd like to do, and it will do a decent job of it. Most importantly, because of re-use of components internally, you will have a consistent UI change, rather than having to patch-up every component one-by-one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions