#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::sync::Arc;
use oxiui_core::{
ButtonResponse, CheckboxResponse, Color, DropdownResponse, Key, Modifiers, MouseButton,
Palette, Size, SliderResponse, TextInputResponse, UiCtx, UiError, UiEvent, Widget,
WidgetResponse,
};
use oxiui_theme::DesignTokens;
pub struct EguiUiCtx<'a> {
ui: &'a mut egui::Ui,
last_response: Option<egui::Response>,
id_seq: usize,
}
impl<'a> EguiUiCtx<'a> {
pub fn new(ui: &'a mut egui::Ui) -> Self {
Self {
ui,
last_response: None,
id_seq: 0,
}
}
pub fn response(&self) -> Option<&egui::Response> {
self.last_response.as_ref()
}
fn next_salt(&mut self) -> egui::Id {
let s = self.id_seq;
self.id_seq += 1;
egui::Id::new(("oxiui_widget", s))
}
}
impl<'a> EguiUiCtx<'a> {
pub fn clipboard_get(&self) -> Option<String> {
self.ui.ctx().output(|o| {
o.commands.iter().find_map(|cmd| {
if let egui::OutputCommand::CopyText(text) = cmd {
if text.is_empty() {
None
} else {
Some(text.clone())
}
} else {
None
}
})
})
}
pub fn clipboard_set(&self, text: &str) {
self.ui.ctx().copy_text(text.to_owned());
}
}
impl<'a> UiCtx for EguiUiCtx<'a> {
fn heading(&mut self, text: &str) {
self.ui.heading(text);
}
fn label(&mut self, text: &str) {
self.ui.label(text);
}
fn button(&mut self, label: &str) -> ButtonResponse {
let resp = self.ui.button(label);
ButtonResponse {
clicked: resp.clicked(),
hovered: resp.hovered(),
}
}
fn text_input(&mut self, text: &str) -> TextInputResponse {
let mut s = text.to_owned();
let r = self.ui.add(egui::TextEdit::singleline(&mut s));
let changed = r.changed();
self.last_response = Some(r);
TextInputResponse::supported(s, changed)
}
fn checkbox(&mut self, label: &str, checked: bool) -> CheckboxResponse {
let mut c = checked;
let r = self.ui.checkbox(&mut c, label);
let changed = r.changed();
self.last_response = Some(r);
CheckboxResponse::supported(c, changed)
}
fn slider(&mut self, value: f64, range: std::ops::RangeInclusive<f64>) -> SliderResponse {
let mut v = value;
let r = self.ui.add(egui::Slider::new(&mut v, range));
let changed = r.changed();
self.last_response = Some(r);
SliderResponse::supported(v, changed)
}
fn dropdown(&mut self, options: &[&str], selected: usize) -> DropdownResponse {
let mut sel = selected.min(options.len().saturating_sub(1));
let salt = self.next_salt();
let r =
egui::ComboBox::from_id_salt(salt)
.show_index(self.ui, &mut sel, options.len(), |i| options[i]);
let changed = r.changed();
self.last_response = Some(r);
DropdownResponse::supported(sel, changed)
}
fn image(&mut self, uri: &str, size: Option<Size>) -> WidgetResponse {
let mut img = egui::Image::from_uri(uri.to_owned());
if let Some(s) = size {
img = img.fit_to_exact_size(egui::vec2(s.width, s.height));
}
let r = self.ui.add(img);
self.last_response = Some(r);
WidgetResponse::supported()
}
fn separator(&mut self) -> WidgetResponse {
let r = self.ui.separator();
self.last_response = Some(r);
WidgetResponse::supported()
}
fn spacer(&mut self, size: f32) -> WidgetResponse {
self.ui.add_space(size);
self.last_response = None;
WidgetResponse::supported()
}
fn scroll_area(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
egui::ScrollArea::vertical().show(self.ui, |ui| {
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
self.last_response = None;
WidgetResponse::supported()
}
fn tooltip(&mut self, text: &str) -> WidgetResponse {
if let Some(r) = self.last_response.take() {
self.last_response = Some(r.on_hover_text(text));
WidgetResponse::supported()
} else {
WidgetResponse::unsupported()
}
}
fn popup(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let ctx = self.ui.ctx().clone();
let salt = self.next_salt();
egui::Window::new("")
.id(egui::Id::new(("oxiui_popup", salt)))
.title_bar(false)
.resizable(false)
.show(&ctx, |ui| {
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
self.last_response = None;
WidgetResponse::supported()
}
fn modal(&mut self, title: &str, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let ctx = self.ui.ctx().clone();
let salt = self.next_salt();
let title = title.to_owned();
egui::Modal::new(egui::Id::new(("oxiui_modal", salt))).show(&ctx, |ui| {
ui.heading(&title);
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
self.last_response = None;
WidgetResponse::supported()
}
fn horizontal(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.ui.horizontal(|ui| {
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
self.last_response = None;
WidgetResponse::supported()
}
fn vertical(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.ui.vertical(|ui| {
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
self.last_response = None;
WidgetResponse::supported()
}
fn grid(&mut self, cols: usize, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let salt = self.next_salt();
egui::Grid::new(salt).num_columns(cols).show(self.ui, |ui| {
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
self.last_response = None;
WidgetResponse::supported()
}
fn menu_bar(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
egui::MenuBar::new().ui(self.ui, |ui| {
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
self.last_response = None;
WidgetResponse::supported()
}
fn rich_text(&mut self, spans: &[oxiui_core::RichTextSpan]) -> WidgetResponse {
use egui::text::{LayoutJob, TextFormat};
let mut job = LayoutJob::default();
for span in spans {
let color = egui::Color32::from_rgba_unmultiplied(
span.color[0],
span.color[1],
span.color[2],
span.color[3],
);
let font_id = egui::FontId::proportional(span.font_size);
job.append(
&span.text,
0.0,
TextFormat {
color,
font_id,
italics: span.italic,
..Default::default()
},
);
}
let r = self.ui.label(job);
self.last_response = Some(r);
WidgetResponse::supported()
}
fn drag_source(&mut self, id: u64, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.ui.dnd_drag_source(egui::Id::new(id), id, |ui| {
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
self.last_response = None;
WidgetResponse::supported()
}
fn drop_target(
&mut self,
accept_ids: &[u64],
content: &mut dyn FnMut(&mut dyn UiCtx),
) -> WidgetResponse {
let (_inner, payload) = self
.ui
.dnd_drop_zone::<u64, ()>(egui::Frame::default(), |ui| {
let mut child = EguiUiCtx::new(ui);
content(&mut child);
});
let _accepted = payload
.as_deref()
.map(|p| accept_ids.contains(p))
.unwrap_or(false);
self.last_response = None;
WidgetResponse::supported()
}
}
fn map_key(key: &Key) -> egui::Key {
match key {
Key::Enter => egui::Key::Enter,
Key::Tab => egui::Key::Tab,
Key::Space => egui::Key::Space,
Key::Backspace => egui::Key::Backspace,
Key::Delete => egui::Key::Delete,
Key::Escape => egui::Key::Escape,
Key::ArrowLeft => egui::Key::ArrowLeft,
Key::ArrowRight => egui::Key::ArrowRight,
Key::ArrowUp => egui::Key::ArrowUp,
Key::ArrowDown => egui::Key::ArrowDown,
Key::Home => egui::Key::Home,
Key::End => egui::Key::End,
Key::PageUp => egui::Key::PageUp,
Key::PageDown => egui::Key::PageDown,
Key::Function(n) => map_function_key(*n),
Key::Character(s) => egui::Key::from_name(s.as_str()).unwrap_or(egui::Key::F12),
Key::Named(s) => egui::Key::from_name(s.as_str()).unwrap_or(egui::Key::F12),
_ => egui::Key::F12,
}
}
fn map_function_key(n: u8) -> egui::Key {
match n {
1 => egui::Key::F1,
2 => egui::Key::F2,
3 => egui::Key::F3,
4 => egui::Key::F4,
5 => egui::Key::F5,
6 => egui::Key::F6,
7 => egui::Key::F7,
8 => egui::Key::F8,
9 => egui::Key::F9,
10 => egui::Key::F10,
11 => egui::Key::F11,
12 => egui::Key::F12,
13 => egui::Key::F13,
14 => egui::Key::F14,
15 => egui::Key::F15,
16 => egui::Key::F16,
17 => egui::Key::F17,
18 => egui::Key::F18,
19 => egui::Key::F19,
20 => egui::Key::F20,
21 => egui::Key::F21,
22 => egui::Key::F22,
23 => egui::Key::F23,
24 => egui::Key::F24,
25 => egui::Key::F25,
26 => egui::Key::F26,
27 => egui::Key::F27,
28 => egui::Key::F28,
29 => egui::Key::F29,
30 => egui::Key::F30,
31 => egui::Key::F31,
32 => egui::Key::F32,
33 => egui::Key::F33,
34 => egui::Key::F34,
35 => egui::Key::F35,
_ => egui::Key::F12,
}
}
fn map_modifiers(m: &Modifiers) -> egui::Modifiers {
egui::Modifiers {
alt: m.alt,
ctrl: m.ctrl,
shift: m.shift,
mac_cmd: false,
command: m.ctrl || m.meta,
}
}
fn map_mouse_button(b: &MouseButton) -> egui::PointerButton {
match b {
MouseButton::Left => egui::PointerButton::Primary,
MouseButton::Right => egui::PointerButton::Secondary,
MouseButton::Middle => egui::PointerButton::Middle,
MouseButton::Other(_) => egui::PointerButton::Extra1,
}
}
pub fn palette_to_egui_visuals(palette: &Palette) -> egui::Visuals {
fn c(col: &Color) -> egui::Color32 {
egui::Color32::from_rgba_unmultiplied(col.0, col.1, col.2, col.3)
}
let mut v = egui::Visuals::dark();
v.override_text_color = Some(c(&palette.text));
v.panel_fill = c(&palette.background);
v.window_fill = c(&palette.surface);
v.selection.bg_fill = c(&palette.primary);
v.hyperlink_color = c(&palette.primary);
v
}
pub fn palette_to_egui_visuals_with_tokens(
palette: &Palette,
tokens: &DesignTokens,
) -> egui::Style {
use oxiui_theme::{RadiusStep, SpacingStep};
let mut visuals = palette_to_egui_visuals(palette);
let radius_val = tokens.radius(RadiusStep::Md).round().clamp(0.0, 255.0) as u8;
let corner_radius = egui::CornerRadius::same(radius_val);
visuals.menu_corner_radius = corner_radius;
visuals.window_corner_radius = corner_radius;
let spacing_val = tokens.spacing(SpacingStep::Sm);
let spacing = egui::style::Spacing {
item_spacing: egui::vec2(spacing_val, spacing_val),
..egui::style::Spacing::default()
};
egui::Style {
visuals,
spacing,
..egui::Style::default()
}
}
pub fn tokens_to_egui_style(
tokens: &oxiui_theme::DesignTokens,
typography: &oxiui_theme::TypographyScale,
) -> egui::Style {
use egui::{FontFamily, FontId, TextStyle};
use oxiui_theme::{RadiusStep, SpacingStep};
let mut style = egui::Style::default();
let spacing_xs = tokens.spacing(SpacingStep::Xs);
let spacing_sm = tokens.spacing(SpacingStep::Sm);
style.spacing.item_spacing = egui::vec2(spacing_sm, spacing_xs);
style.spacing.button_padding = egui::vec2(spacing_sm, spacing_xs / 2.0);
let radius_sm =
egui::CornerRadius::same(tokens.radius(RadiusStep::Sm).round().clamp(0.0, 255.0) as u8);
let radius_md =
egui::CornerRadius::same(tokens.radius(RadiusStep::Md).round().clamp(0.0, 255.0) as u8);
style.visuals.widgets.noninteractive.corner_radius = radius_sm;
style.visuals.widgets.inactive.corner_radius = radius_sm;
style.visuals.widgets.active.corner_radius = radius_md;
style.visuals.menu_corner_radius = radius_md;
style.visuals.window_corner_radius = radius_md;
style.text_styles.insert(
TextStyle::Heading,
FontId::new(typography.headline.size, FontFamily::Proportional),
);
style.text_styles.insert(
TextStyle::Body,
FontId::new(typography.body.size, FontFamily::Proportional),
);
style.text_styles.insert(
TextStyle::Button,
FontId::new(typography.body.size, FontFamily::Proportional),
);
style.text_styles.insert(
TextStyle::Monospace,
FontId::new(typography.body.size, FontFamily::Monospace),
);
style.text_styles.insert(
TextStyle::Small,
FontId::new(typography.caption.size, FontFamily::Proportional),
);
style
}
pub fn forward_event_to_egui(ctx: &egui::Context, event: &UiEvent) {
match event {
UiEvent::ImePreedit { text, cursor: _ } => {
ctx.input_mut(|i| {
i.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
});
}
UiEvent::ImeCommit(text) => {
ctx.input_mut(|i| {
i.events
.push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone())));
});
}
UiEvent::KeyDown {
key,
modifiers,
repeat,
} => {
ctx.input_mut(|i| {
i.events.push(egui::Event::Key {
key: map_key(key),
physical_key: None,
pressed: true,
repeat: *repeat,
modifiers: map_modifiers(modifiers),
});
});
}
UiEvent::KeyUp { key, modifiers } => {
ctx.input_mut(|i| {
i.events.push(egui::Event::Key {
key: map_key(key),
physical_key: None,
pressed: false,
repeat: false,
modifiers: map_modifiers(modifiers),
});
});
}
UiEvent::KeyPress(name) => {
if let Some(egui_key) = egui::Key::from_name(name.as_str()) {
ctx.input_mut(|i| {
i.events.push(egui::Event::Key {
key: egui_key,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers::default(),
});
});
}
}
UiEvent::MouseMove { x, y } => {
ctx.input_mut(|i| {
i.events.push(egui::Event::PointerMoved(egui::pos2(*x, *y)));
});
}
UiEvent::Mouse { x, y } => {
ctx.input_mut(|i| {
i.events.push(egui::Event::PointerMoved(egui::pos2(*x, *y)));
});
}
UiEvent::MouseDown {
button,
x,
y,
modifiers,
} => {
ctx.input_mut(|i| {
i.events.push(egui::Event::PointerButton {
pos: egui::pos2(*x, *y),
button: map_mouse_button(button),
pressed: true,
modifiers: map_modifiers(modifiers),
});
});
}
UiEvent::MouseUp {
button,
x,
y,
modifiers,
} => {
ctx.input_mut(|i| {
i.events.push(egui::Event::PointerButton {
pos: egui::pos2(*x, *y),
button: map_mouse_button(button),
pressed: false,
modifiers: map_modifiers(modifiers),
});
});
}
UiEvent::Resize(_w, _h) => {
}
_ => {}
}
}
pub struct OxiWidget<'a> {
widget: &'a mut dyn Widget,
}
impl<'a> OxiWidget<'a> {
pub fn new(widget: &'a mut dyn Widget) -> Self {
Self { widget }
}
}
impl<'a> egui::Widget for OxiWidget<'a> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let mut ctx = EguiUiCtx::new(ui);
self.widget.render(&mut ctx);
let maybe_resp = ctx.last_response.take();
drop(ctx);
maybe_resp.unwrap_or_else(|| ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()))
}
}
pub fn load_font_into_egui(ctx: &egui::Context, font_bytes: Vec<u8>) -> Result<(), UiError> {
oxiui_text::TextPipeline::from_bytes(&font_bytes)
.map_err(|e| UiError::Render(format!("invalid font bytes: {e}")))?;
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"OxiFont".to_owned(),
Arc::new(egui::FontData::from_owned(font_bytes)),
);
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "OxiFont".to_owned());
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.push("OxiFont".to_owned());
ctx.set_fonts(fonts);
Ok(())
}
pub fn load_fonts_into_egui(
family_map: &[(&str, Vec<u8>)],
ctx: &egui::Context,
) -> Result<(), UiError> {
let mut fonts = egui::FontDefinitions::default();
for (family, bytes) in family_map {
oxiui_text::TextPipeline::from_bytes(bytes)
.map_err(|e| UiError::Render(format!("invalid font bytes for '{family}': {e}")))?;
let name = format!("OxiFont-{family}");
fonts.font_data.insert(
name.clone(),
Arc::new(egui::FontData::from_owned(bytes.clone())),
);
fonts
.families
.entry(egui::FontFamily::Name(name.clone().into()))
.or_default()
.push(name);
}
ctx.set_fonts(fonts);
Ok(())
}
pub struct EguiAdapter {
palette: Option<Palette>,
}
impl EguiAdapter {
pub fn new() -> Self {
Self { palette: None }
}
pub fn with_palette(mut self, p: Palette) -> Self {
self.palette = Some(p);
self
}
pub fn build(self) -> impl Fn(&egui::Context) {
let palette = self.palette;
move |ctx: &egui::Context| {
if let Some(p) = &palette {
ctx.set_visuals(palette_to_egui_visuals(p));
}
}
}
}
impl Default for EguiAdapter {
fn default() -> Self {
Self::new()
}
}
fn palettes_equal(a: &Palette, b: &Palette) -> bool {
a.background == b.background
&& a.surface == b.surface
&& a.primary == b.primary
&& a.on_primary == b.on_primary
&& a.text == b.text
&& a.muted == b.muted
}
pub struct StatefulEguiAdapter {
palette: Option<Palette>,
cached_visuals: Option<(Palette, egui::Visuals)>,
pending_font_bytes: Option<Vec<u8>>,
fonts_loaded: bool,
design_tokens: Option<oxiui_theme::DesignTokens>,
typography: Option<oxiui_theme::TypographyScale>,
tokens_applied: bool,
pub visuals_recompute_count: usize,
pub fonts_load_count: usize,
}
impl StatefulEguiAdapter {
pub fn new() -> Self {
Self {
palette: None,
cached_visuals: None,
pending_font_bytes: None,
fonts_loaded: false,
design_tokens: None,
typography: None,
tokens_applied: false,
visuals_recompute_count: 0,
fonts_load_count: 0,
}
}
pub fn with_palette(mut self, p: Palette) -> Self {
self.palette = Some(p);
self
}
pub fn with_design_tokens(
mut self,
tokens: oxiui_theme::DesignTokens,
typography: oxiui_theme::TypographyScale,
) -> Self {
self.design_tokens = Some(tokens);
self.typography = Some(typography);
self
}
pub fn with_font_bytes(mut self, bytes: Vec<u8>) -> Self {
self.pending_font_bytes = Some(bytes);
self
}
pub fn set_palette(&mut self, p: Palette) {
self.palette = Some(p);
}
pub fn apply(&mut self, ctx: &egui::Context) {
if !self.fonts_loaded {
if let Some(bytes) = self.pending_font_bytes.take() {
let _ = load_font_into_egui(ctx, bytes);
self.fonts_load_count += 1;
}
self.fonts_loaded = true;
}
if !self.tokens_applied {
if let (Some(ref tok), Some(ref typo)) = (&self.design_tokens, &self.typography) {
let mut style = tokens_to_egui_style(tok, typo);
if let Some(ref p) = self.palette {
style.visuals = palette_to_egui_visuals(p);
use oxiui_theme::RadiusStep;
let radius_sm = egui::CornerRadius::same(
tok.radius(RadiusStep::Sm).round().clamp(0.0, 255.0) as u8,
);
let radius_md = egui::CornerRadius::same(
tok.radius(RadiusStep::Md).round().clamp(0.0, 255.0) as u8,
);
style.visuals.widgets.noninteractive.corner_radius = radius_sm;
style.visuals.widgets.inactive.corner_radius = radius_sm;
style.visuals.widgets.active.corner_radius = radius_md;
style.visuals.menu_corner_radius = radius_md;
style.visuals.window_corner_radius = radius_md;
let visuals = style.visuals.clone();
self.cached_visuals = Some((p.clone(), visuals));
self.visuals_recompute_count += 1;
}
ctx.set_global_style(style);
self.tokens_applied = true;
return;
}
}
if let Some(ref current) = self.palette {
let needs_update = match &self.cached_visuals {
None => true,
Some((ref last, _)) => !palettes_equal(last, current),
};
if needs_update {
let visuals = palette_to_egui_visuals(current);
ctx.set_visuals(visuals.clone());
self.cached_visuals = Some((current.clone(), visuals));
self.visuals_recompute_count += 1;
}
}
}
}
impl Default for StatefulEguiAdapter {
fn default() -> Self {
Self::new()
}
}