#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use egui::util::IdTypeMap;
use egui::{Pos2, Response, Ui};
use taffy::prelude::*;
use widgets::TaffySeparator;
pub use taffy;
pub mod widgets;
mod egui_widgets;
pub fn tui(ui: &mut egui::Ui, id: impl Into<egui::Id>) -> TuiInitializer<'_> {
TuiInitializer {
ui,
id: id.into(),
allocated_rect: None,
available_space: Size {
width: AvailableSpace::MinContent,
height: AvailableSpace::MinContent,
},
style: Default::default(),
known_size: Size {
width: None,
height: None,
},
}
}
#[must_use]
pub struct TuiInitializer<'a> {
ui: &'a mut egui::Ui,
allocated_rect: Option<egui::Rect>,
available_space: Size<AvailableSpace>,
known_size: Size<Option<f32>>,
style: taffy::Style,
id: egui::Id,
}
impl<'a> TuiInitializer<'a> {
pub fn with_allocated_rect(mut self, rect: egui::Rect) -> TuiInitializer<'a> {
self.allocated_rect = Some(rect);
self.available_space = Size {
width: AvailableSpace::Definite(rect.width()),
height: AvailableSpace::Definite(rect.height()),
};
self
}
pub fn reserve_space(self, space: egui::Vec2) -> TuiInitializer<'a> {
self.reserve_width(space.x).reserve_height(space.y)
}
pub fn reserve_width(mut self, width: f32) -> TuiInitializer<'a> {
self.ui.set_min_width(width);
self.available_space.width = AvailableSpace::Definite(width);
self.known_size.width = Some(width);
self
}
pub fn reserve_height(mut self, height: f32) -> TuiInitializer<'a> {
self.ui.set_min_height(height);
self.available_space.height = AvailableSpace::Definite(height);
self.known_size.height = Some(height);
self
}
pub fn reserve_available_space(self) -> TuiInitializer<'a> {
self.reserve_available_width().reserve_available_height()
}
pub fn reserve_available_width(self) -> TuiInitializer<'a> {
let width = self.ui.available_size().x;
self.reserve_width(width)
}
pub fn reserve_available_height(self) -> TuiInitializer<'a> {
let height = self.ui.available_size().y;
self.reserve_height(height)
}
pub fn with_available_space(
mut self,
available_space: Size<AvailableSpace>,
) -> TuiInitializer<'a> {
self.available_space = available_space;
self
}
pub fn style(mut self, style: taffy::Style) -> TuiInitializer<'a> {
self.style = style;
self
}
pub fn show<T>(self, f: impl FnOnce(&mut Tui<'_>) -> T) -> T {
let ui = self.ui;
let output = Tui::create(
ui,
self.id,
ui.available_rect_before_wrap(),
Some(self.available_space),
self.style,
|tui| {
tui.set_limit_scroll_area_size(Some(0.7));
f(tui)
},
);
if self.allocated_rect.is_none() {
let size = output.container.layout.content_size;
ui.allocate_space(egui::Vec2 {
x: size.width,
y: size.height,
});
}
output.inner
}
}
pub struct Tui<'a> {
main_id: egui::Id,
ui: &'a mut Ui,
current_id: egui::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>>,
limit_scroll_area_size: Option<f32>,
}
impl<'a> Tui<'a> {
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 create<T>(
ui: &'a mut Ui,
id: egui::Id,
root_rect: egui::Rect,
available_space: Option<Size<AvailableSpace>>,
style: Style,
f: impl FnOnce(&mut Tui<'_>) -> 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,
limit_scroll_area_size: None,
};
this.tui().id(id).style(style).add(|state| {
let resp = f(state);
let container = state.recalculate();
TaffyReturn {
inner: resp,
container,
}
})
}
pub fn set_limit_scroll_area_size(&mut self, size: Option<f32>) {
self.limit_scroll_area_size = size;
}
fn add_child_node(&mut self, id: egui::Id, style: taffy::Style) -> (NodeId, TaffyContainerUi) {
if self.used_items.contains(&id) {
log::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)
})
}
fn add_children_inner<'r, T>(
&'r mut self,
id: TuiId,
style: Style,
content: Option<impl FnOnce(&mut egui::Ui)>,
f: impl FnOnce(&mut Tui<'a>) -> T,
) -> T {
let id = id.resolve(self);
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
}
fn add_container<T>(
&mut self,
id: impl Into<TuiId>,
style: taffy::Style,
content: impl FnOnce(&mut Ui, TaffyContainerUi) -> TuiContainerResponse<T>,
) -> T {
let id = id.into().resolve(self);
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());
ui_builder.layout.as_mut().unwrap().main_dir = egui::Direction::TopDown;
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,
};
if state.taffy.get_node_context(nodeid) != Some(&new_content) {
state
.taffy
.set_node_context(nodeid, Some(new_content))
.unwrap();
}
});
resp.inner
}
fn add_scroll_area_ext<T>(
&mut self,
id: impl Into<TuiId>,
mut style: taffy::Style,
limit: Option<f32>,
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 let Some(limit) = limit {
style.max_size.height = Dimension::Length(self.root_rect.height() * limit);
style.max_size.width = Dimension::Length(self.root_rect.width() * limit);
}
self.tui().id(id).style(style).add(|tui| {
let layout = Self::with_state(tui.main_id, tui.ui.ctx().clone(), |state| {
*state.taffy.layout(tui.current_node.unwrap()).unwrap()
});
let style = taffy::Style {
..Default::default()
};
tui.add_container("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,
};
TuiContainerResponse {
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| {
state.items.retain(|k, v| {
if self.used_items.contains(k) {
return true;
}
if let Some(parent) = state.taffy.parent(*v) {
state.taffy.remove_child(parent, *v).unwrap();
}
state.taffy.remove(*v).unwrap();
false
});
self.used_items.clear();
let taffy = &mut state.taffy;
if taffy.dirty(current_node).unwrap() || state.last_size != root_rect.size() {
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,
infinite: egui::Vec2b::FALSE,
});
let Context {
mut min_size,
mut max_size,
infinite,
} = context;
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: if infinite.x {
root_rect.width()
} else {
max_size.x
},
y: if infinite.y {
root_rect.height()
} else {
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 };
final_size
},
)
.unwrap();
log::trace!("Taffy recalculation done!");
self.ui.ctx().request_discard("Taffy recalculation");
}
TaffyContainerUi {
parent_rect: root_rect,
layout: state.layout(current_node),
first_frame: false,
}
})
}
#[inline]
pub fn egui_ui(&self) -> &&'a mut Ui {
&self.ui
}
pub fn root_rect(&self) -> egui::Rect {
self.root_rect
}
#[inline]
pub fn egui_style_mut(&mut self) -> &mut egui::Style {
self.ui.style_mut()
}
}
pub struct TaffyReturn<T> {
pub inner: T,
pub container: TaffyContainerUi,
}
#[derive(PartialEq, Default, Clone, Copy)]
struct Context {
min_size: egui::Vec2,
max_size: egui::Vec2,
infinite: egui::Vec2b,
}
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 TuiContainerResponse<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,
}
pub trait TuiWidget {
type Response;
fn taffy_ui(self, tuib: TuiBuilder) -> Self::Response;
}
#[derive(Default)]
pub enum TuiId {
Hiarchy(egui::Id),
Unique(egui::Id),
#[default]
Auto,
}
impl TuiId {
fn resolve(self, tui: &Tui) -> egui::Id {
match self {
TuiId::Hiarchy(id) => tui.current_id.with(id),
TuiId::Unique(id) => id,
TuiId::Auto => tui.current_id.with("auto").with(tui.current_node_index),
}
}
}
impl From<egui::Id> for TuiId {
#[inline]
fn from(value: egui::Id) -> Self {
Self::Hiarchy(value)
}
}
impl From<&str> for TuiId {
#[inline]
fn from(value: &str) -> Self {
Self::Hiarchy(egui::Id::new(value))
}
}
#[inline]
pub fn tid<T>(id: T) -> TuiId
where
T: std::hash::Hash,
{
TuiId::Hiarchy(egui::Id::new(id))
}
struct TaffyState {
taffy: TaffyTree<Context>,
last_size: egui::Vec2,
items: HashMap<egui::Id, NodeId>,
}
impl TaffyState {
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()
}
}
#[must_use]
pub struct TuiBuilder<'r, 'a>
where
'a: 'r,
{
tui: &'r mut Tui<'a>,
id: TuiId,
style: Option<taffy::Style>,
}
pub trait AsTuiBuilder<'r, 'a>: Sized
where
'a: 'r,
{
fn tui(self) -> TuiBuilder<'r, 'a>;
}
impl<'r, 'a> AsTuiBuilder<'r, 'a> for &'r mut Tui<'a>
where
'a: 'r,
{
#[inline]
fn tui(self) -> TuiBuilder<'r, 'a> {
TuiBuilder {
tui: self,
style: None,
id: TuiId::Auto,
}
}
}
impl<'r, 'a> AsTuiBuilder<'r, 'a> for TuiBuilder<'r, 'a>
where
'a: 'r,
{
#[inline]
fn tui(self) -> TuiBuilder<'r, 'a> {
self
}
}
impl<'r, 'a, T> TuiBuilderLogic<'r, 'a> for T
where
T: AsTuiBuilder<'r, 'a>,
'a: 'r,
{
}
pub trait TuiBuilderLogic<'r, 'a>: AsTuiBuilder<'r, 'a> + Sized
where
'a: 'r,
{
#[inline]
fn id(self, id: impl Into<TuiId>) -> TuiBuilder<'r, 'a> {
let mut tui = self.tui();
tui.id = id.into();
tui
}
#[inline]
fn style(self, style: taffy::Style) -> TuiBuilder<'r, 'a> {
let mut tui = self.tui();
tui.style = Some(style);
tui
}
#[inline]
fn id_style(self, id: impl Into<TuiId>, style: taffy::Style) -> TuiBuilder<'r, 'a> {
let mut tui = self.tui();
tui.id = id.into();
tui.style = Some(style);
tui
}
#[inline]
fn mut_style(self, f: impl FnOnce(&mut taffy::Style)) -> TuiBuilder<'r, 'a> {
let mut tui = self.tui();
f(tui.style.get_or_insert_default());
tui
}
fn add<T>(self, f: impl FnOnce(&mut Tui<'_>) -> T) -> T {
let tui = self.tui();
tui.tui.add_children_inner(
tui.id,
tui.style.unwrap_or_default(),
Option::<fn(&mut egui::Ui)>::None,
f,
)
}
fn add_with_background<T>(self, f: impl FnOnce(&mut Tui<'_>) -> T) -> T {
self.add_with_background_ui(
|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());
});
},
f,
)
}
fn add_with_border<T>(self, f: impl FnOnce(&mut Tui<'_>) -> T) -> T {
self.add_with_background_ui(
|ui| {
egui::Frame::group(ui.style()).show(ui, |ui| {
let available_space = ui.available_size();
let (_id, _rect) = ui.allocate_space(available_space);
});
},
f,
)
}
fn add_with_background_ui<T>(
self,
content: impl FnOnce(&mut egui::Ui),
f: impl FnOnce(&mut Tui<'_>) -> T,
) -> T {
let tui = self.tui();
tui.tui
.add_children_inner(tui.id, tui.style.unwrap_or_default(), Some(content), f)
}
fn add_scroll_area_with_background<T>(self, content: impl FnOnce(&mut Ui) -> T) -> T {
let mut tui = self.tui();
tui = tui.mut_style(|style| {
style.min_size = taffy::Size {
width: Dimension::Length(0.),
height: Dimension::Length(0.),
};
});
tui.add_with_background(move |tui| {
let s = LengthPercentageAuto::Length(
0.3 * tui.ui.text_style_height(&egui::TextStyle::Body),
);
let style = taffy::Style {
margin: Rect {
left: s,
right: s,
top: s,
bottom: s,
},
..Default::default()
};
tui.tui().style(style).add_scroll_area(content)
})
}
fn add_scroll_area<T>(self, content: impl FnOnce(&mut Ui) -> T) -> T {
let tui = self.tui();
let limit = tui.tui.limit_scroll_area_size;
tui.add_scroll_area_ext(limit, content)
}
fn add_scroll_area_ext<T>(self, limit: Option<f32>, content: impl FnOnce(&mut Ui) -> T) -> T {
let tui = self.tui();
tui.tui
.add_scroll_area_ext(tui.id, tui.style.unwrap_or_default(), limit, content)
}
#[inline]
fn ui<T>(self, content: impl FnOnce(&mut Ui) -> T) -> T {
self.ui_finite(content)
}
fn ui_finite<T>(self, content: impl FnOnce(&mut Ui) -> T) -> T {
self.ui_manual(|ui, _params| {
let inner = content(ui);
TuiContainerResponse {
inner,
min_size: ui.min_size(),
intrinsic_size: None,
max_size: ui.min_size(),
infinite: egui::Vec2b::FALSE,
scroll_area: false,
}
})
}
fn ui_infinite<T>(self, content: impl FnOnce(&mut Ui) -> T) -> T {
self.ui_manual(|ui, _params| {
let inner = content(ui);
TuiContainerResponse {
inner,
min_size: ui.min_size(),
intrinsic_size: None,
max_size: ui.min_size(),
infinite: egui::Vec2b::TRUE,
scroll_area: false,
}
})
}
fn ui_manual<T>(
self,
content: impl FnOnce(&mut Ui, TaffyContainerUi) -> TuiContainerResponse<T>,
) -> T {
let tui = self.tui();
tui.tui
.add_container(tui.id, tui.style.unwrap_or_default(), content)
}
#[inline]
fn ui_add<T: TuiWidget>(self, widget: T) -> T::Response {
widget.taffy_ui(self.tui())
}
fn ui_add_manual(
self,
f: impl FnOnce(&mut egui::Ui) -> Response,
transform: impl FnOnce(
TuiContainerResponse<Response>,
&egui::Ui,
) -> TuiContainerResponse<Response>,
) -> Response {
self.ui_manual(|ui, _params| {
let response = f(ui);
let resp = TuiContainerResponse {
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)
})
}
#[inline]
fn label(self, text: impl Into<egui::WidgetText>) -> Response {
egui::Label::new(text).taffy_ui(self.tui())
}
#[inline]
fn heading(self, text: impl Into<egui::RichText>) -> Response {
egui::Label::new(text.into().heading()).taffy_ui(self.tui())
}
#[inline]
fn separator(self) -> Response {
TaffySeparator::default().taffy_ui(self.tui())
}
}