Skip to content

Instantly share code, notes, and snippets.

@PPakalns
Created November 10, 2024 14:15
Show Gist options
  • Save PPakalns/337d0539f7b01bd8216c627c0f65a30c to your computer and use it in GitHub Desktop.
Save PPakalns/337d0539f7b01bd8216c627c0f65a30c to your computer and use it in GitHub Desktop.
Egui integration with taffy.

Idea based on lucasmerlin ideas https://github.com/lucasmerlin/hello_egui/tree/main/crates

  1. to integrate taffy with egui (egui_taffy) long before egui 0.29 .
  2. to implement flex layout manually (egui_flex) into egui using egui 0.29 new features (request_discard and intrinsic size)

In second attempt more ergonomic API was made where closures didn't need to be stored and could reference mutable data from outer scope.

Taffy can be nicely integrated into egui using egui 0.29 new functionality and similar API as in egui_flex. We do not need to reimplement layouting logic.

See following code example. Still need to clean up this code to make crate out of it, but I am already using it in my personal project.

use core::f32;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use egui::util::IdTypeMap;
use egui::{Button, Id, Pos2, Response, Ui};
use taffy::prelude::*;
pub use taffy;
struct TaffyState {
taffy: TaffyTree<Context>,
last_size: egui::Vec2,
items: HashMap<egui::Id, NodeId>,
}
impl TaffyState {
pub fn new() -> Self {
Self {
taffy: TaffyTree::new(),
last_size: egui::Vec2::ZERO,
items: HashMap::default(),
}
}
fn layout(&self, node_id: NodeId) -> Layout {
*self.taffy.layout(node_id).unwrap()
}
}
pub struct TaffyPass<'a> {
main_id: Id,
ui: &'a mut Ui,
current_id: Id,
current_node: Option<NodeId>,
current_node_index: usize,
last_child_count: usize,
parent_rect: egui::Rect,
used_items: HashSet<egui::Id>,
root_rect: egui::Rect,
available_space: Option<Size<AvailableSpace>>,
}
impl<'a> TaffyPass<'a> {
#[inline]
pub fn root_id(&self) -> Id {
self.main_id
}
#[inline]
pub fn current_id(&self) -> Id {
self.current_id
}
fn with_state<T>(id: egui::Id, ctx: egui::Context, f: impl FnOnce(&mut TaffyState) -> T) -> T {
let state = ctx.data_mut(|data: &mut IdTypeMap| {
let state: Arc<Mutex<TaffyState>> = data
.get_temp_mut_or_insert_with(id, || Arc::new(Mutex::new(TaffyState::new())))
.clone();
state
});
let mut state = state.lock().unwrap();
f(&mut state)
}
pub fn new<T>(
ui: &'a mut Ui,
id: Id,
root_rect: egui::Rect,
available_space: Option<Size<AvailableSpace>>,
style: Style,
f: impl FnOnce(&mut TaffyPass<'a>) -> T,
) -> TaffyReturn<T> {
let mut this = Self {
main_id: id,
ui,
current_node: None,
current_node_index: 0,
last_child_count: 0,
parent_rect: root_rect,
used_items: Default::default(),
root_rect,
available_space,
current_id: id,
};
this.add_children(id, style, |state| {
let resp = f(state);
let container = state.recalculate();
TaffyReturn {
inner: resp,
container,
}
})
}
fn add_child_node(&mut self, id: egui::Id, style: taffy::Style) -> (NodeId, TaffyContainerUi) {
if self.used_items.contains(&id) {
tracing::error!("Taffy layout id collision!");
}
Self::with_state(self.main_id, self.ui.ctx().clone(), |state| {
let child_idx = self.current_node_index;
self.current_node_index += 1;
self.used_items.insert(id);
let mut first_frame = false;
let node_id = if let Some(node_id) = state.items.get(&id).copied() {
if state.taffy.style(node_id).unwrap() != &style {
state.taffy.set_style(node_id, style).unwrap();
}
node_id
} else {
first_frame = true;
let node = state.taffy.new_leaf(style).unwrap();
state.items.insert(id, node);
node
};
if let Some(current_node) = self.current_node {
if child_idx < self.last_child_count {
if state.taffy.child_at_index(current_node, child_idx).unwrap() != node_id {
state
.taffy
.replace_child_at_index(current_node, child_idx, node_id)
.unwrap();
}
} else {
state.taffy.add_child(current_node, node_id).unwrap();
self.last_child_count += 1;
}
}
let container = TaffyContainerUi {
layout: state.layout(node_id),
parent_rect: self.parent_rect,
first_frame,
};
(node_id, container)
})
}
pub fn add_children<T>(
&mut self,
id: egui::Id,
style: Style,
f: impl FnOnce(&mut TaffyPass<'a>) -> T,
) -> T {
self.add_children_inner(id, style, Option::<fn(&mut egui::Ui)>::None, f)
}
pub fn add_children_with_background<T>(
&mut self,
id: egui::Id,
style: Style,
f: impl FnOnce(&mut TaffyPass<'a>) -> T,
) -> T {
self.add_children_with_ui(
id,
style,
|ui| {
egui::Frame::popup(ui.style()).show(ui, |ui| {
let available_space = ui.available_size();
let (id, rect) = ui.allocate_space(available_space);
let _response = ui.interact(rect, id, egui::Sense::click_and_drag());
// Must catch mouse events
});
},
f,
)
}
pub fn add_children_with_ui<T>(
&mut self,
id: egui::Id,
style: Style,
content: impl FnOnce(&mut egui::Ui),
f: impl FnOnce(&mut TaffyPass<'a>) -> T,
) -> T {
self.add_children_inner(id, style, Some(content), f)
}
fn add_children_inner<T>(
&mut self,
id: egui::Id,
style: Style,
content: Option<impl FnOnce(&mut egui::Ui)>,
f: impl FnOnce(&mut TaffyPass<'a>) -> T,
) -> T {
let (node_id, render_options) = self.add_child_node(id, style);
let stored_id = self.current_id;
let stored_node = self.current_node;
let stored_node_index = self.current_node_index;
let stored_last_child_count = self.last_child_count;
let stored_parent_rect = self.parent_rect;
Self::with_state(self.main_id, self.ui.ctx().clone(), |state| {
self.current_node = Some(node_id);
self.current_node_index = 0;
self.last_child_count = state.taffy.child_count(node_id);
let max_rect = render_options.full_container();
self.parent_rect = if max_rect.any_nan() {
self.parent_rect
} else {
max_rect
};
self.current_id = id;
});
if let Some(content) = content {
let max_rect = render_options.full_container();
if !max_rect.any_nan() {
let mut child_ui = self.ui.new_child(
egui::UiBuilder::new()
.id_salt(id.with("background"))
.max_rect(max_rect),
);
content(&mut child_ui);
}
}
let resp = f(self);
Self::with_state(self.main_id, self.ui.ctx().clone(), |state| {
let mut current_cnt = state.taffy.child_count(node_id);
while current_cnt > self.last_child_count {
state
.taffy
.remove_child_at_index(node_id, current_cnt - 1)
.unwrap();
current_cnt -= 1;
}
});
self.current_id = stored_id;
self.current_node = stored_node;
self.current_node_index = stored_node_index;
self.last_child_count = stored_last_child_count;
self.parent_rect = stored_parent_rect;
resp
}
pub fn add_simple<T>(
&mut self,
id: egui::Id,
style: taffy::Style,
content: impl FnOnce(&mut Ui) -> T,
) -> T {
self.add_simple_finite(id, style, content)
}
pub fn add_simple_finite<T>(
&mut self,
id: egui::Id,
style: taffy::Style,
content: impl FnOnce(&mut Ui) -> T,
) -> T {
self.add_container(id, style, |ui, _params| {
let inner = content(ui);
TaffyContainerResponse {
inner,
min_size: ui.min_size(),
intrinsic_size: None,
max_size: ui.min_size(),
infinite: egui::Vec2b::FALSE,
scroll_area: false,
}
})
}
pub fn add_simple_infinite<T>(
&mut self,
id: egui::Id,
style: taffy::Style,
content: impl FnOnce(&mut Ui) -> T,
) -> T {
self.add_container(id, style, |ui, _params| {
let inner = content(ui);
TaffyContainerResponse {
inner,
min_size: ui.min_size(),
intrinsic_size: None,
max_size: ui.min_size(),
infinite: egui::Vec2b::TRUE,
scroll_area: false,
}
})
}
pub fn add_container<T>(
&mut self,
id: egui::Id,
style: taffy::Style,
content: impl FnOnce(&mut Ui, TaffyContainerUi) -> TaffyContainerResponse<T>,
) -> T {
let parent_node = self.current_node.unwrap();
let (nodeid, mut render_options) = self.add_child_node(id, style.clone());
let mut ui_builder = egui::UiBuilder::new()
.max_rect(render_options.inner_container())
.id_salt(id.with("_ui"))
.layout(Default::default());
let parent_style = Self::with_state(self.main_id, self.ui.ctx().clone(), |state| {
state.taffy.style(parent_node).unwrap().clone()
});
// Inner boxes are always vertical by default
ui_builder.layout.as_mut().unwrap().main_dir = egui::Direction::TopDown;
if parent_style.flex_direction == FlexDirection::Column {
for align_item in [parent_style.align_items, style.align_self] {
match align_item {
Some(align) => match align {
AlignItems::Start => {}
AlignItems::End => {}
AlignItems::FlexStart => {}
AlignItems::FlexEnd => {}
AlignItems::Center => {
ui_builder.layout = Some(
ui_builder
.layout
.unwrap_or_default()
.with_cross_align(egui::Align::Center),
);
}
AlignItems::Baseline => todo!(),
AlignItems::Stretch => {
ui_builder.layout = Some(
ui_builder
.layout
.unwrap_or_default()
.with_cross_justify(true),
);
}
},
None => {}
}
}
}
// TODO: Handle correctly case where max_rect has NaN values
if ui_builder.max_rect.unwrap().any_nan() {
render_options.first_frame = true;
ui_builder = ui_builder.max_rect(self.parent_rect);
}
if render_options.first_frame {
ui_builder = ui_builder.sizing_pass().invisible();
}
let mut child_ui = self.ui.new_child(ui_builder);
let resp = content(&mut child_ui, render_options);
Self::with_state(self.main_id, self.ui.ctx().clone(), |state| {
let min_size = if let Some(intrinsic_size) = resp.intrinsic_size {
resp.min_size.min(intrinsic_size).ceil()
} else {
resp.min_size.ceil()
};
let mut max_size = resp.max_size;
max_size = max_size.max(min_size);
let new_content = Context {
min_size,
max_size,
infinite: resp.infinite,
scroll_area: resp.scroll_area,
};
if state.taffy.get_node_context(nodeid) != Some(&new_content) {
state
.taffy
.set_node_context(nodeid, Some(new_content))
.unwrap();
}
});
resp.inner
}
pub fn add_scroll_area_with_background<T>(
&mut self,
id: egui::Id,
mut style: taffy::Style,
content: impl FnOnce(&mut Ui) -> T,
) -> T {
style.min_size = taffy::Size {
width: Dimension::Length(0.),
height: Dimension::Length(0.),
};
self.add_children_with_background(id, style, move |taffy| {
let s = LengthPercentageAuto::Length(
0.3 * taffy.ui.text_style_height(&egui::TextStyle::Body),
);
let mut style = taffy::Style::default();
style.margin = Rect {
left: s,
right: s,
top: s,
bottom: s,
};
taffy.add_scroll_area(taffy.id_with("taffy_inner_scroll_area"), style, content)
})
}
pub fn add_scroll_area<T>(
&mut self,
id: egui::Id,
style: taffy::Style,
content: impl FnOnce(&mut Ui) -> T,
) -> T {
self.add_scroll_area_ext(id, style, true, content)
}
pub fn add_scroll_area_ext<T>(
&mut self,
id: egui::Id,
mut style: taffy::Style,
limit: bool,
content: impl FnOnce(&mut Ui) -> T,
) -> T {
style.overflow = taffy::Point {
x: taffy::Overflow::Visible,
y: taffy::Overflow::Hidden,
};
style.display = taffy::Display::Block;
style.min_size = Size {
width: Dimension::Length(0.),
height: Dimension::Length(0.),
};
if limit {
style.max_size.height = Dimension::Length(self.root_rect.height() * 0.7);
}
self.add_children(id, style, |taffy| {
let layout = Self::with_state(taffy.main_id, taffy.ui.ctx().clone(), |state| {
state
.taffy
.layout(taffy.current_node.unwrap())
.unwrap()
.clone()
});
let style = taffy::Style {
..Default::default()
};
taffy.add_container(taffy.id_with("inner"), style, |ui, _params| {
let mut real_min_size = None;
let scroll_area = egui::ScrollArea::both()
.id_salt(ui.id().with("scroll_area"))
.max_width(ui.available_width())
.min_scrolled_width(layout.size.width)
.max_width(layout.size.width)
.min_scrolled_height(layout.size.height)
.max_height(layout.size.height)
.show(ui, |ui| {
let resp = content(ui);
real_min_size = Some(ui.min_size());
resp
});
let potential_frame_size = scroll_area.content_size;
let max_size = egui::Vec2 {
x: potential_frame_size.x,
y: potential_frame_size.y,
};
TaffyContainerResponse {
inner: scroll_area.inner,
min_size: real_min_size.unwrap_or(max_size),
intrinsic_size: None,
max_size,
infinite: egui::Vec2b::FALSE,
scroll_area: true,
}
})
})
// })
}
fn recalculate(&mut self) -> TaffyContainerUi {
let root_rect = self.root_rect;
let available_space = self.available_space.unwrap_or(Size {
width: AvailableSpace::Definite(root_rect.width()),
height: AvailableSpace::Definite(root_rect.height()),
});
let current_node = self.current_node.unwrap();
Self::with_state(self.main_id, self.ui.ctx().clone(), |state| {
// Remove unused nodes
state.items.retain(|k, v| {
if self.used_items.contains(k) {
return true;
}
if let Some(parent) = state.taffy.parent(*v) {
// Mark parent nodes "dirty"
state.taffy.remove_child(parent, *v).unwrap();
}
state.taffy.remove(*v).unwrap();
return false;
});
self.used_items.clear();
let taffy = &mut state.taffy;
if taffy.dirty(current_node).unwrap() || state.last_size != root_rect.size() {
// let ctx = self.ui.ctx();
state.last_size = root_rect.size();
taffy
.compute_layout_with_measure(
current_node,
available_space,
|_known_size: Size<Option<f32>>,
available_space: Size<AvailableSpace>,
_id,
context,
_style|
-> Size<f32> {
let context = context.copied().unwrap_or(Context {
min_size: egui::Vec2::ZERO,
max_size: egui::Vec2::ZERO,
scroll_area: false,
infinite: egui::Vec2b::FALSE,
});
let Context {
mut min_size,
mut max_size,
scroll_area: _,
infinite,
} = context;
// if scroll_area {
// min_size = egui::Vec2::ZERO;
// }
if min_size.any_nan() {
min_size = egui::Vec2::ZERO;
}
if max_size.any_nan() {
max_size = root_rect.size();
}
let max_size = egui::Vec2 {
x: infinite
.x
.then_some(root_rect.width())
.unwrap_or(max_size.x),
y: infinite
.y
.then_some(root_rect.height())
.unwrap_or(max_size.y),
};
let width = match available_space.width {
AvailableSpace::Definite(num) => {
num.clamp(min_size.x, max_size.x.max(min_size.x))
}
AvailableSpace::MinContent => min_size.x,
AvailableSpace::MaxContent => max_size.x,
};
let height = match available_space.height {
AvailableSpace::Definite(num) => {
num.clamp(min_size.y, max_size.y.max(min_size.y))
}
AvailableSpace::MinContent => min_size.y,
AvailableSpace::MaxContent => max_size.y,
};
#[allow(clippy::let_and_return)]
let final_size = Size { width, height };
// println!(
// "{:?} {:?} {:?} {:?} {:?} {:?}",
// _id, min_size, max_size, available_space, final_size, _known_size,
// );
final_size
},
)
.unwrap();
// taffy.print_tree(current_node);
tracing::trace!("Taffy recalculation done!");
self.ui.ctx().request_discard("Taffy recalculation");
}
TaffyContainerUi {
parent_rect: root_rect,
layout: state.layout(current_node),
first_frame: false,
}
})
}
pub fn add_widget_with_transform_response_style(
&mut self,
f: impl FnOnce(&mut egui::Ui) -> Response,
transform: impl FnOnce(
TaffyContainerResponse<Response>,
&egui::Ui,
) -> TaffyContainerResponse<Response>,
style: Style,
) -> Response {
self.add_container(
self.current_id().with(self.current_node_index),
style,
|ui, _params| {
let response = f(ui);
let resp = TaffyContainerResponse {
min_size: response.rect.size(),
intrinsic_size: response.intrinsic_size,
max_size: response.rect.size(),
infinite: egui::Vec2b::FALSE,
inner: response,
scroll_area: false,
};
transform(resp, ui)
},
)
}
pub fn add_widget_with_transform_response(
&mut self,
f: impl FnOnce(&mut egui::Ui) -> Response,
transform: impl FnOnce(
TaffyContainerResponse<Response>,
&egui::Ui,
) -> TaffyContainerResponse<Response>,
) -> Response {
self.add_widget_with_transform_response_style(f, transform, Default::default())
}
#[inline]
pub fn add_widget_with_transform(
&mut self,
widget: impl egui::Widget,
transform: impl FnOnce(
TaffyContainerResponse<Response>,
&egui::Ui,
) -> TaffyContainerResponse<Response>,
) -> Response {
self.add_widget_with_transform_response(|ui| ui.add(widget), transform)
}
#[inline]
pub fn add_widget_with_transform_style(
&mut self,
widget: impl egui::Widget,
style: Style,
transform: impl FnOnce(
TaffyContainerResponse<Response>,
&egui::Ui,
) -> TaffyContainerResponse<Response>,
) -> Response {
self.add_widget_with_transform_response_style(|ui| ui.add(widget), transform, style)
}
#[inline]
pub fn add_widget(&mut self, widget: impl egui::Widget) -> Response {
self.add_widget_with_transform(widget, identity_transform)
}
#[inline]
pub fn add_widget_with_response(&mut self, ui: impl FnOnce(&mut Ui) -> Response) -> Response {
self.add_widget_with_transform_response(ui, identity_transform)
}
#[inline]
pub fn label(&mut self, text: impl Into<egui::WidgetText>) -> Response {
egui::Label::new(text).taffy_ui(self)
}
pub fn heading(&mut self, text: impl Into<egui::RichText>) -> Response {
egui::Label::new(text.into().heading()).taffy_ui(self)
}
pub fn separator(&mut self) -> Response {
TaffySeparator::default().taffy_ui(self)
}
#[inline]
pub fn ui(&self) -> &&'a mut Ui {
&self.ui
}
#[inline]
pub fn id_with(&self, child: impl std::hash::Hash) -> egui::Id {
self.current_id().with(child)
}
pub fn root_rect(&self) -> egui::Rect {
self.root_rect
}
}
fn identity_transform<T>(
value: TaffyContainerResponse<T>,
_ui: &egui::Ui,
) -> TaffyContainerResponse<T> {
value
}
pub struct TaffyReturn<T> {
pub inner: T,
pub container: TaffyContainerUi,
}
#[derive(PartialEq, Default, Clone, Copy)]
struct Context {
pub min_size: egui::Vec2,
pub max_size: egui::Vec2,
pub scroll_area: bool,
infinite: egui::Vec2b,
}
/// Helper to show the inner content of a container.
pub struct TaffyContainerUi {
parent_rect: egui::Rect,
layout: taffy::Layout,
first_frame: bool,
}
impl TaffyContainerUi {
pub fn full_container(&self) -> egui::Rect {
let layout = &self.layout;
let rect = egui::Rect::from_min_size(
Pos2::new(layout.location.x, layout.location.y),
egui::Vec2::new(layout.size.width, layout.size.height),
);
rect.translate(self.parent_rect.min.to_vec2())
}
pub fn inner_container(&self) -> egui::Rect {
let layout = &self.layout;
let size = layout.size
- Size {
width: layout.padding.left + layout.padding.right,
height: layout.padding.top + layout.padding.bottom,
};
let rect = egui::Rect::from_min_size(
Pos2::new(
layout.location.x + layout.padding.left,
layout.location.y + layout.padding.top,
),
egui::Vec2::new(size.width, size.height),
);
rect.translate(self.parent_rect.min.to_vec2())
}
}
pub struct TaffyContainerResponse<T> {
pub inner: T,
pub min_size: egui::Vec2,
pub intrinsic_size: Option<egui::Vec2>,
pub max_size: egui::Vec2,
pub infinite: egui::Vec2b,
pub scroll_area: bool,
}
/// Implement this trait for a widget to make it usable in a flex container.
///
/// The reason there is a separate trait is that we need to measure the content size independently
/// of the frame size. (The content will stay at it's intrinsic size while the frame will be
/// stretched according to the flex layout.)
///
/// If your widget has no frmae you don't need to implement this trait and can use
/// [`crate::TaffyWidget`].
///
/// Trait idea taken from egui_flex
pub trait TaffyWidget {
/// The response type of the widget
type Response;
/// Show your widget here. Use the provided [`Ui`] to draw the container (e.g. using a [`egui::Frame`])
/// and in the frame ui use [`TaffyContainerUi::content`] to draw your widget.
fn taffy_ui(self, taffy: &mut TaffyPass) -> Self::Response;
}
mod egui_widgets {
use super::{TaffyPass, TaffyWidget};
use egui::widgets::{
Checkbox, DragValue, Hyperlink, Image, ImageButton, Label, Link, ProgressBar, RadioButton,
SelectableLabel, Slider, Spinner, TextEdit,
};
macro_rules! impl_widget {
($($widget:ty),*) => {
$(
impl TaffyWidget for $widget {
type Response = egui::Response;
fn taffy_ui(self, taffy: &mut TaffyPass) -> Self::Response {
taffy.add_widget(self)
}
}
)*
};
}
impl_widget!(
Label,
Checkbox<'_>,
Image<'_>,
DragValue<'_>,
Hyperlink,
ImageButton<'_>,
ProgressBar,
RadioButton,
Link,
SelectableLabel,
Slider<'_>,
TextEdit<'_>,
Spinner
);
}
impl TaffyWidget for Button<'_> {
type Response = egui::Response;
fn taffy_ui(self, taffy: &mut TaffyPass) -> Self::Response {
taffy.add_widget_with_transform(self, |mut val, _ui| {
val.infinite.x = true;
val
})
}
}
#[derive(Default)]
pub struct TaffySeparator {
is_horizontal_line: Option<bool>,
separator: egui::Separator,
}
impl TaffySeparator {
pub fn vertical(mut self) -> Self {
self.is_horizontal_line = Some(false);
self.separator = self.separator.vertical();
self
}
}
impl egui::Widget for TaffySeparator {
fn ui(self, ui: &mut Ui) -> Response {
ui.add(self.separator)
}
}
impl TaffyWidget for TaffySeparator {
type Response = egui::Response;
fn taffy_ui(self, taffy: &mut TaffyPass) -> Self::Response {
let mut style = taffy::Style::default();
style.min_size = Size {
width: Dimension::Length(0.),
height: Dimension::Length(0.),
};
let is_horizontal_line = self.is_horizontal_line;
taffy.add_widget_with_transform_style(self, style, |mut space, ui| {
let is_horizontal_line =
is_horizontal_line.unwrap_or_else(|| !ui.layout().main_dir().is_horizontal());
if let Some(size) = space.intrinsic_size.as_mut() {
match is_horizontal_line {
true => {
size.x = 0.;
space.infinite.x = true;
}
false => {
size.y = 0.;
space.infinite.y = true;
}
}
}
space
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment