From 19c4a7212c22c4831deb2f93206ee95062d8ac02 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 19 Aug 2026 02:48:50 +0530 Subject: [PATCH 1/2] Upgrade egui stack 0.29 -> 0.35 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical migration only — no behavior or API-surface changes beyond what the egui upgrade forced. Dependency bumps: - egui 0.29 -> 0.35 - egui_extras 0.29 -> 0.35 - eframe 0.29 -> 0.35 (dev-dependency) - egui-phosphor 0.7 -> 0.13 (the release pinned to egui ^0.35) API migrations: - Rounding -> CornerRadius (now i8-backed); helper fns cast the f32 radius tokens `as u8` - Frame::none() -> Frame::new() / Frame::NONE - Margin::same/symmetric now take i8, not f32 — cast at call sites - WidgetVisuals::rounding -> corner_radius; Visuals::window_rounding / menu_rounding -> window_corner_radius / menu_corner_radius - Painter::rect_stroke now requires an explicit StrokeKind; used StrokeKind::Inside everywhere (egui team's stated migration default for closed rectangles, matching this crate's prior stroke look) - Context::style()/set_style() removed in favor of per-theme style_of()/style_mut_of(); theme::apply() now pins the active theme via ctx.set_theme() and fully rewrites that theme's Style in one style_mut_of() closure, preserving the old single-Style override behavior - egui::Id::new / id_salt now require AsId (Hash + Debug) — all `id_source: impl Hash` params gained a `+ std::fmt::Debug` bound - ctx.fonts(|f| f.layout_job(..)) -> ctx.fonts_mut(..) (layout_job needs a mutable FontsView) - TextEdit::frame(bool) -> TextEdit::frame(Frame); false -> Frame::NONE - egui::popup module / Memory::{is_popup_open,toggle_popup} removed — ported menu_button/select to the new egui::Popup builder (from_toggle_button_response + close_behavior); open-state reads use Popup::is_id_open - Context::screen_rect() removed -> content_rect() - eframe::App::update -> App::ui (gallery example only) Docs: AGENTS.md / README.md version claims updated to match. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 13 ++-- Cargo.toml | 8 +- README.md | 7 +- examples/gallery.rs | 19 ++--- src/brand.rs | 2 +- src/components.rs | 184 +++++++++++++++++++++++++------------------- src/theme.rs | 94 ++++++++++++---------- src/tokens.rs | 18 ++--- 8 files changed, 194 insertions(+), 151 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 07e4f0c..27d347d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ ## What this is -`tokito_ui` is a small **egui 0.29** component library — the shared design +`tokito_ui` is a small **egui 0.35** component library — the shared design layer for the Tokito desktop schematic studio ([github.com/TokitoAI/tokito](https://github.com/TokitoAI/tokito)). @@ -47,12 +47,15 @@ src/ `FontFamily::Name("phosphor")` so its Private-Use-Area codepoints never collide with a text font (Inter Var occupies part of the PUA range). - **Stable widget ids.** Anything with internal egui state (`text_input`, - `search_field`) takes an explicit `id_source: impl Hash`. Never derive a - widget id from layout position — it collides and breaks focus on reflow. + `search_field`) takes an explicit `id_source: impl Hash + std::fmt::Debug` + (egui 0.35's `Id::new` requires `AsId`, which needs both bounds). Never + derive a widget id from layout position — it collides and breaks focus on + reflow. - **`Tokens` is `#[non_exhaustive]`.** Construct via `Tokens::dark()` / `light()` and assign fields to customise; new fields won't be breaking. -- **Version coupling.** Pin `egui` 0.29 and `egui-phosphor` 0.7.x together - (≥ 0.8 targets egui ≥ 0.30). Bumping is a coordinated change. +- **Version coupling.** Pin `egui` 0.35 and `egui-phosphor` 0.13.x together — + check crates.io for the `egui-phosphor` release whose `egui` dependency + matches before bumping either. Bumping is a coordinated change. ## STRICT — this repo is the *only* place UI components are defined diff --git a/Cargo.toml b/Cargo.toml index 5a6c075..2a1bbc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,13 +11,13 @@ keywords = ["egui", "eframe", "gui", "components", "design-system"] categories = ["gui", "rendering::gui"] [dependencies] -egui = "0.29" -egui-phosphor = { version = "0.7", default-features = false, features = ["regular"] } -egui_extras = "0.29" +egui = "0.35" +egui-phosphor = { version = "0.13", default-features = false, features = ["regular"] } +egui_extras = "0.35" image = { version = "0.25", default-features = false, features = ["png"] } [dev-dependencies] -eframe = "0.29" +eframe = "0.35" [lints.clippy] too_many_arguments = "allow" diff --git a/README.md b/README.md index 8d721e1..62fdecd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ buttons, inputs, menus, toggles — plus light/dark theming and Phosphor icons. A small, opinionated UI component library and design system for Rust GUI apps.** -`tokito_ui` is an **egui 0.29 component library**: a flat colour-token palette, +`tokito_ui` is an **egui 0.35 component library**: a flat colour-token palette, a theme applier, icon helpers, and a set of composable widget primitives. It is the shared design layer of the [Tokito](https://github.com/TokitoAI/tokito) desktop schematic studio, and @@ -36,8 +36,9 @@ Browse it live: `cargo run --example gallery`. tokito_ui = { git = "https://github.com/TokitoAI/ui" } ``` -`tokito_ui` pins `egui` **0.29** and `egui-phosphor` **0.7.x**. Bump all three -together (egui-phosphor ≥ 0.8 targets egui ≥ 0.30). +`tokito_ui` pins `egui` **0.35** and `egui-phosphor` **0.13.x**. Bump all three +together — check crates.io for the `egui-phosphor` release that depends on +your target `egui` version before bumping either. ## Setup — once, at startup diff --git a/examples/gallery.rs b/examples/gallery.rs index 5626604..00bdab4 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -67,21 +67,22 @@ impl Gallery { } impl eframe::App for Gallery { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + let ctx = ui.ctx().clone(); let t = if self.dark { Tokens::dark() } else { Tokens::light() }; - tokito_ui::theme::apply(ctx, &t); + tokito_ui::theme::apply(&ctx, &t); egui::CentralPanel::default() .frame( - egui::Frame::none() + egui::Frame::new() .fill(t.bg) - .inner_margin(egui::Margin::same(28.0)), + .inner_margin(egui::Margin::same((28.0) as i8)), ) - .show(ctx, |ui| { + .show(ui, |ui| { egui::ScrollArea::vertical() .auto_shrink([false, false]) .show(ui, |ui| self.body(ui, &t)); @@ -93,7 +94,7 @@ impl eframe::App for Gallery { self.frame += 1; ctx.request_repaint(); if self.frame == 4 { - ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); + ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::default())); } let shot = ctx.input(|i| { i.raw.events.iter().find_map(|e| match e { @@ -341,9 +342,9 @@ impl Gallery { egui::vec2(380.0, 260.0), egui::Layout::top_down(egui::Align::Min).with_cross_justify(true), |ui| { - egui::Frame::none() + egui::Frame::new() .fill(t.bg) - .inner_margin(egui::Margin::same(8.0)) + .inner_margin(egui::Margin::same((8.0) as i8)) .show(ui, |ui| { egui::ScrollArea::vertical() .id_salt("gallery_chat_history") @@ -428,7 +429,7 @@ impl Gallery { }); // Help button (floating bottom-right of the example window). - let area_pos = ui.ctx().screen_rect().right_bottom() - egui::vec2(24.0, 24.0); + let area_pos = ui.ctx().content_rect().right_bottom() - egui::vec2(24.0, 24.0); egui::Area::new(egui::Id::new("gallery_help")) .order(egui::Order::Foreground) .fixed_pos(area_pos - egui::vec2(32.0, 32.0)) diff --git a/src/brand.rs b/src/brand.rs index db8c2a7..6df3f22 100644 --- a/src/brand.rs +++ b/src/brand.rs @@ -58,7 +58,7 @@ pub fn brand_tile(ui: &mut Ui, side: f32) -> Response { let painter = ui.painter(); let tile_fill = Color32::from_rgb(0x07, 0x10, 0x1e); let radius = side * 0.235; // matches the Figma's 36 / 150 corner ratio. - painter.rect_filled(rect, egui::Rounding::same(radius), tile_fill); + painter.rect_filled(rect, egui::CornerRadius::same(radius as u8), tile_fill); // Inset the mark to ~70 % of the tile, centred. The mark's native // aspect is slightly taller than square, so derive the rect from diff --git a/src/components.rs b/src/components.rs index 3014df3..135b38e 100644 --- a/src/components.rs +++ b/src/components.rs @@ -53,7 +53,12 @@ pub fn card(ui: &mut Ui, t: &Tokens, size: Vec2, add_contents: impl FnOnce(&mut let border = lerp_color(t.border, t.border_strong, hv); let painter = ui.painter(); painter.rect_filled(rect, t.rounding_md(), fill); - painter.rect_stroke(rect.shrink(0.5), t.rounding_md(), Stroke::new(1.0, border)); + painter.rect_stroke( + rect.shrink(0.5), + t.rounding_md(), + Stroke::new(1.0, border), + egui::StrokeKind::Inside, + ); let mut content = ui.new_child( UiBuilder::new() @@ -131,6 +136,7 @@ pub fn icon_button(ui: &mut Ui, t: &Tokens, glyph: &str, side: f32, ink: Color32 rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.border.gamma_multiply(hv)), + egui::StrokeKind::Inside, ); } ui.painter().text( @@ -185,8 +191,12 @@ pub fn text_button( }; ui.painter().rect_filled(rect, t.rounding_sm(), fill); if let Some(b) = border { - ui.painter() - .rect_stroke(rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, b)); + ui.painter().rect_stroke( + rect.shrink(0.5), + t.rounding_sm(), + Stroke::new(1.0, b), + egui::StrokeKind::Inside, + ); } ui.painter() .galley(rect.center() - galley.size() / 2.0, galley, ink); @@ -218,6 +228,7 @@ pub fn badge(ui: &mut Ui, t: &Tokens, text: &str) -> Response { rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.border), + egui::StrokeKind::Inside, ); ui.painter() .galley(rect.center() - galley.size() / 2.0, galley, t.text_3); @@ -237,26 +248,20 @@ pub fn badge(ui: &mut Ui, t: &Tokens, text: &str) -> Response { pub fn menu_button( ui: &mut Ui, t: &Tokens, - id_source: impl Hash, + id_source: impl Hash + std::fmt::Debug, glyph: &str, side: f32, add_items: impl FnOnce(&mut Ui), ) -> Response { let trigger = icon_button(ui, t, glyph, side, t.text_2); let popup_id = egui::Id::new(id_source); - if trigger.clicked() { - ui.memory_mut(|m| m.toggle_popup(popup_id)); - } - egui::popup::popup_below_widget( - ui, - popup_id, - &trigger, - egui::PopupCloseBehavior::CloseOnClick, - |ui| { + egui::Popup::from_toggle_button_response(&trigger) + .id(popup_id) + .close_behavior(egui::PopupCloseBehavior::CloseOnClick) + .show(|ui| { ui.set_min_width(184.0); add_items(ui); - }, - ); + }); trigger } @@ -292,7 +297,7 @@ pub fn list_row(ui: &mut Ui, t: &Tokens, job: egui::text::LayoutJob, selected: b if bg.a() > 0 { ui.painter().rect_filled(rect, t.rounding_sm(), bg); } - let galley = ui.fonts(|f| f.layout_job(job)); + let galley = ui.fonts_mut(|f| f.layout_job(job)); let pos = pos2(rect.left() + 10.0, rect.center().y - galley.size().y / 2.0); ui.painter().galley(pos, galley, t.text); response @@ -307,7 +312,7 @@ pub fn list_row(ui: &mut Ui, t: &Tokens, job: egui::text::LayoutJob, selected: b pub fn text_input( ui: &mut Ui, t: &Tokens, - id_source: impl Hash, + id_source: impl Hash + std::fmt::Debug, value: &mut String, hint: &str, width: f32, @@ -331,7 +336,7 @@ pub fn text_input( pub fn secret_input( ui: &mut Ui, t: &Tokens, - id_source: impl Hash, + id_source: impl Hash + std::fmt::Debug, value: &mut String, hint: &str, width: f32, @@ -354,7 +359,7 @@ pub fn secret_input( pub fn search_field( ui: &mut Ui, t: &Tokens, - id_source: impl Hash, + id_source: impl Hash + std::fmt::Debug, query: &mut String, hint: &str, width: f32, @@ -388,8 +393,12 @@ fn bordered_input( let focused = ui.memory(|m| m.has_focus(id)); let border = if focused { t.accent } else { t.border }; ui.painter().rect_filled(rect, t.rounding_sm(), t.bg_chrome); - ui.painter() - .rect_stroke(rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, border)); + ui.painter().rect_stroke( + rect.shrink(0.5), + t.rounding_sm(), + Stroke::new(1.0, border), + egui::StrokeKind::Inside, + ); let text_left = if let Some(glyph) = leading_glyph { ui.painter().text( pos2(rect.left() + 12.0, rect.center().y), @@ -418,7 +427,7 @@ fn bordered_input( // weak text, which in our dark tokens is nearly full-ink and // reads as real content. .hint_text(egui::RichText::new(hint).color(t.text_3)) - .frame(false) + .frame(egui::Frame::NONE) .password(mask) .desired_width(edit_rect.width()), ) @@ -448,7 +457,7 @@ pub fn toggle(ui: &mut Ui, t: &Tokens, value: &mut bool, label: &str) -> Respons let track_rect = Rect::from_min_size(rect.min, track); ui.painter().rect_filled( track_rect, - egui::Rounding::same(track.y / 2.0), + egui::CornerRadius::same((track.y / 2.0) as u8), lerp_color(t.border_strong, t.accent, on), ); let knob_x = egui::lerp((track_rect.left() + 11.0)..=(track_rect.right() - 11.0), on); @@ -496,7 +505,7 @@ pub fn modal( return; } - let screen = ctx.screen_rect(); + let screen = ctx.content_rect(); // Dimmed backdrop — a full-screen click target that closes the modal. let backdrop = egui::Area::new(egui::Id::new(("tokito_ui_modal_backdrop", title))) .order(egui::Order::Foreground) @@ -516,11 +525,11 @@ pub fn modal( .order(egui::Order::Foreground) .anchor(egui::Align2::CENTER_CENTER, [0.0, -20.0]) .show(ctx, |ui| { - egui::Frame::none() + egui::Frame::new() .fill(t.bg_chrome) .stroke(Stroke::new(1.0, t.border_strong)) - .rounding(t.rounding_md()) - .inner_margin(egui::Margin::same(t.space_4)) + .corner_radius(t.rounding_md()) + .inner_margin(egui::Margin::same((t.space_4) as i8)) .show(ui, |ui| { ui.set_width(width); ui.horizontal(|ui| { @@ -688,6 +697,7 @@ pub fn checkbox( box_rect.shrink(0.5), t.rounding_xs(), Stroke::new(1.0, border), + egui::StrokeKind::Inside, ); if on > 0.01 { let c = box_rect.center(); @@ -737,6 +747,7 @@ pub fn segmented( rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.border), + egui::StrokeKind::Inside, ); let n = options.len().max(1); @@ -790,14 +801,14 @@ pub fn segmented( pub fn select( ui: &mut Ui, t: &Tokens, - id_source: impl Hash, + id_source: impl Hash + std::fmt::Debug, current: &str, width: f32, add_options: impl FnOnce(&mut Ui), ) -> Response { let (rect, response) = ui.allocate_exact_size(vec2(width, 34.0), Sense::click()); let popup_id = egui::Id::new(id_source); - let open = ui.memory(|m| m.is_popup_open(popup_id)); + let open = egui::Popup::is_id_open(ui.ctx(), popup_id); let hv = hover_t(ui, response.id, response.hovered() || open); ui.painter().rect_filled(rect, t.rounding_sm(), t.bg_chrome); @@ -806,8 +817,12 @@ pub fn select( } else { lerp_color(t.border, t.border_strong, hv) }; - ui.painter() - .rect_stroke(rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, border)); + ui.painter().rect_stroke( + rect.shrink(0.5), + t.rounding_sm(), + Stroke::new(1.0, border), + egui::StrokeKind::Inside, + ); ui.painter().text( pos2(rect.left() + 11.0, rect.center().y), egui::Align2::LEFT_CENTER, @@ -825,19 +840,13 @@ pub fn select( if response.hovered() { ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); } - if response.clicked() { - ui.memory_mut(|m| m.toggle_popup(popup_id)); - } - egui::popup::popup_below_widget( - ui, - popup_id, - &response, - egui::PopupCloseBehavior::CloseOnClick, - |ui| { + egui::Popup::from_toggle_button_response(&response) + .id(popup_id) + .close_behavior(egui::PopupCloseBehavior::CloseOnClick) + .show(|ui| { ui.set_min_width(width); add_options(ui); - }, - ); + }); response } @@ -937,6 +946,7 @@ pub fn banner( rect.shrink(0.5), t.rounding_md(), Stroke::new(1.0, accent.gamma_multiply(0.55)), + egui::StrokeKind::Inside, ); ui.painter().text( pos2( @@ -964,7 +974,7 @@ pub fn banner( pub fn collapsing( ui: &mut Ui, t: &Tokens, - id_source: impl Hash, + id_source: impl Hash + std::fmt::Debug, label: &str, add_body: impl FnOnce(&mut Ui), ) { @@ -1094,7 +1104,7 @@ where }; painter.rect_filled(rect, t.rounding_sm(), fill); - painter.rect_stroke(rect, t.rounding_sm(), stroke); + painter.rect_stroke(rect, t.rounding_sm(), stroke, egui::StrokeKind::Inside); let ink = if selected { t.accent @@ -1223,7 +1233,7 @@ pub fn sortable_header( pub fn data_table( ui: &mut Ui, t: &Tokens, - id_source: impl Hash, + id_source: impl Hash + std::fmt::Debug, headers: &[&str], cols: Vec, state: &mut SortState, @@ -1474,8 +1484,8 @@ pub fn toast_overlay(ctx: &egui::Context, t: &Tokens, stack: &mut ToastStack) { egui::Frame::popup(ui.style()) .fill(t.card) .stroke(Stroke::new(1.0, accent)) - .rounding(t.rounding_md()) - .inner_margin(egui::Margin::symmetric(12.0, 10.0)) + .corner_radius(t.rounding_md()) + .inner_margin(egui::Margin::symmetric((12.0) as i8, (10.0) as i8)) .show(ui, |ui| { ui.set_width(300.0); ui.horizontal(|ui| { @@ -1528,7 +1538,7 @@ pub fn chip(ui: &mut Ui, t: &Tokens, label: &str, selected: bool) -> bool { egui::Button::new(RichText::new(label).size(11.0).color(ink)) .fill(fill) .stroke(Stroke::new(1.0, stroke_color)) - .rounding(t.rounding_sm()) + .corner_radius(t.rounding_sm()) .min_size(vec2(0.0, 28.0)), ); resp.clicked() @@ -1545,10 +1555,10 @@ pub fn chip(ui: &mut Ui, t: &Tokens, label: &str, selected: bool) -> bool { /// size: it grows to fit `add_contents`. Width is whatever the parent layout /// gives it. Padding is `space_4` on all sides. pub fn content_card(ui: &mut Ui, t: &Tokens, add_contents: impl FnOnce(&mut Ui)) { - egui::Frame::none() + egui::Frame::new() .fill(t.card) - .rounding(t.rounding_md()) - .inner_margin(egui::Margin::same(t.space_4)) + .corner_radius(t.rounding_md()) + .inner_margin(egui::Margin::same((t.space_4) as i8)) .stroke(Stroke::new(1.0, t.border)) .show(ui, |ui| { add_contents(ui); @@ -1604,10 +1614,10 @@ pub fn list_section_label(ui: &mut Ui, t: &Tokens, label: &str, count: usize) { /// (no search results, no items in the list, no recent files). Centred /// text, soft card background, no border. pub fn empty_state(ui: &mut Ui, t: &Tokens, message: &str) { - egui::Frame::none() + egui::Frame::new() .fill(t.card) - .rounding(t.rounding_sm()) - .inner_margin(egui::Margin::same(14.0)) + .corner_radius(t.rounding_sm()) + .inner_margin(egui::Margin::same((14.0) as i8)) .show(ui, |ui| { ui.centered_and_justified(|ui| { ui.label(RichText::new(message).size(12.0).color(t.text_2)); @@ -1653,9 +1663,9 @@ pub fn app_header( let mut actions = AppHeaderActions::default(); let height = 52.0; - egui::Frame::none() + egui::Frame::new() .fill(t.bg_chrome) - .inner_margin(egui::Margin::symmetric(t.space_3, 0.0)) + .inner_margin(egui::Margin::symmetric((t.space_3) as i8, (0.0) as i8)) .show(ui, |ui| { ui.set_height(height); ui.horizontal_centered(|ui| { @@ -1684,7 +1694,7 @@ pub fn app_header( let resp = ui.add( egui::TextEdit::singleline(project_name) .desired_width(260.0) - .margin(egui::Margin::symmetric(8.0, 4.0)), + .margin(egui::Margin::symmetric((8.0) as i8, (4.0) as i8)), ); if resp.lost_focus() { *is_editing = false; @@ -1751,10 +1761,13 @@ pub enum TabItem<'a> { /// icon + label in muted ink with a subtle hover wash. pub fn tab_bar(ui: &mut Ui, t: &Tokens, items: &[TabItem<'_>], selected: usize) -> Option { let mut clicked = None; - egui::Frame::none() + egui::Frame::new() .fill(t.bg_chrome) .stroke(Stroke::new(1.0, t.border_soft)) - .inner_margin(egui::Margin::symmetric(t.space_3, t.space_1)) + .inner_margin(egui::Margin::symmetric( + (t.space_3) as i8, + (t.space_1) as i8, + )) .show(ui, |ui| { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = t.space_1; @@ -1818,6 +1831,7 @@ fn tab_pill(ui: &mut Ui, t: &Tokens, icon: &str, label: &str, selected: bool) -> rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.accent), + egui::StrokeKind::Inside, ); } @@ -1935,10 +1949,10 @@ pub fn chat_bubble( BubbleKind::Assistant => t.chat_bubble_bg, BubbleKind::User => t.chat_bubble_bg_user, }; - egui::Frame::none() + egui::Frame::new() .fill(fill) - .rounding(t.rounding_sm()) - .inner_margin(egui::Margin::symmetric(14.0, 12.0)) + .corner_radius(t.rounding_sm()) + .inner_margin(egui::Margin::symmetric((14.0) as i8, (12.0) as i8)) .show(ui, |ui| { ui.set_max_width(ui.available_width().min(640.0)); body(ui); @@ -1955,7 +1969,13 @@ pub fn chat_bubble( /// /// `id_source` scopes child ids so multiple activity bubbles can coexist /// without reflow making their animation state fight. -pub fn chat_activity(ui: &mut Ui, t: &Tokens, id_source: impl Hash, label: &str, detail: &str) { +pub fn chat_activity( + ui: &mut Ui, + t: &Tokens, + id_source: impl Hash + std::fmt::Debug, + label: &str, + detail: &str, +) { ui.ctx().request_repaint_after(Duration::from_millis(50)); ui.push_id(id_source, |ui| { chat_bubble(ui, t, BubbleKind::Assistant, "", |ui| { @@ -2035,11 +2055,14 @@ pub fn chat_composer( ) -> Option { let mut action = None; - egui::Frame::none() + egui::Frame::new() .fill(t.card) .stroke(Stroke::new(1.0, t.border)) - .rounding(t.rounding_md()) - .inner_margin(egui::Margin::symmetric(t.space_3, t.space_2)) + .corner_radius(t.rounding_md()) + .inner_margin(egui::Margin::symmetric( + (t.space_3) as i8, + (t.space_2) as i8, + )) .show(ui, |ui| { ui.horizontal(|ui| { let send_side = 36.0; @@ -2047,7 +2070,7 @@ pub fn chat_composer( let resp = ui.add_sized( [composer_w, 0.0], egui::TextEdit::multiline(&mut state.text) - .frame(false) + .frame(egui::Frame::NONE) .desired_rows(1) .hint_text(hint), ); @@ -2205,9 +2228,9 @@ pub fn ai_helper_rail( fn collapsed_glyph_rail(ui: &mut Ui, t: &Tokens) -> Option { let mut out = None; - egui::Frame::none() + egui::Frame::new() .fill(t.bg_chrome) - .inner_margin(egui::Margin::symmetric(6.0, t.space_3)) + .inner_margin(egui::Margin::symmetric((6.0) as i8, (t.space_3) as i8)) .show(ui, |ui| { ui.vertical_centered(|ui| { if icon_button(ui, t, icons::ph::SPARKLE, 32.0, t.accent).clicked() { @@ -2225,10 +2248,10 @@ fn expanded_rail( composer: &mut ChatComposerState, ) -> Option { let mut out = None; - egui::Frame::none() + egui::Frame::new() .fill(t.bg_chrome) .stroke(Stroke::new(1.0, t.border_soft)) - .inner_margin(egui::Margin::same(t.space_3)) + .inner_margin(egui::Margin::same((t.space_3) as i8)) .show(ui, |ui| { // Header — title + collapse + close. ui.horizontal(|ui| { @@ -2289,8 +2312,12 @@ fn rail_suggestion(ui: &mut Ui, t: &Tokens, label: &str) -> Response { let fill = lerp_color(t.card, t.card_hover, hv); let border = lerp_color(t.border, t.border_strong, hv); ui.painter().rect_filled(rect, t.rounding_sm(), fill); - ui.painter() - .rect_stroke(rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, border)); + ui.painter().rect_stroke( + rect.shrink(0.5), + t.rounding_sm(), + Stroke::new(1.0, border), + egui::StrokeKind::Inside, + ); ui.painter().text( pos2(rect.left() + 12.0, rect.center().y), egui::Align2::LEFT_CENTER, @@ -2335,6 +2362,7 @@ pub fn thread_row( rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.accent), + egui::StrokeKind::Inside, ); } @@ -2449,10 +2477,10 @@ pub fn conversation_sidebar( body: impl FnOnce(&mut Ui), ) -> Option { let mut out = None; - egui::Frame::none() + egui::Frame::new() .fill(t.bg_chrome) .stroke(Stroke::new(1.0, t.border_soft)) - .inner_margin(egui::Margin::same(t.space_2)) + .inner_margin(egui::Margin::same((t.space_2) as i8)) .show(ui, |ui| { ui.horizontal(|ui| { ui.label( @@ -2622,10 +2650,10 @@ pub fn suggestion_card( let interactive = matches!(status, SuggestionCardStatus::Pending); let mut hover_this_frame: Option = None; - egui::Frame::none() + egui::Frame::new() .fill(t.card) - .rounding(t.rounding_md()) - .inner_margin(egui::Margin::same(t.space_4)) + .corner_radius(t.rounding_md()) + .inner_margin(egui::Margin::same((t.space_4) as i8)) .stroke(Stroke::new(1.0, t.border)) .show(ui, |ui| { // Header row: title + status badge on the right. diff --git a/src/theme.rs b/src/theme.rs index 7141719..21dc5ad 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -57,70 +57,80 @@ pub fn apply(ctx: &Context, t: &Tokens) { visuals.widgets.noninteractive.bg_fill = t.card; visuals.widgets.noninteractive.fg_stroke = Stroke::new(1.0, t.text_3); visuals.widgets.noninteractive.bg_stroke = Stroke::new(1.0, t.border_soft); - visuals.widgets.noninteractive.rounding = t.rounding_sm(); + visuals.widgets.noninteractive.corner_radius = t.rounding_sm(); visuals.widgets.inactive.bg_fill = t.card; visuals.widgets.inactive.weak_bg_fill = t.bg_chrome; visuals.widgets.inactive.fg_stroke = Stroke::new(1.0, t.text_2); visuals.widgets.inactive.bg_stroke = Stroke::new(1.0, t.border); - visuals.widgets.inactive.rounding = t.rounding_sm(); + visuals.widgets.inactive.corner_radius = t.rounding_sm(); visuals.widgets.hovered.bg_fill = t.card_hover; visuals.widgets.hovered.fg_stroke = Stroke::new(1.0, t.text); visuals.widgets.hovered.bg_stroke = Stroke::new(1.0, t.border_strong); - visuals.widgets.hovered.rounding = t.rounding_sm(); + visuals.widgets.hovered.corner_radius = t.rounding_sm(); visuals.widgets.active.bg_fill = t.card_hover; visuals.widgets.active.fg_stroke = Stroke::new(1.0, t.text); visuals.widgets.active.bg_stroke = Stroke::new(1.0, t.accent); - visuals.widgets.active.rounding = t.rounding_sm(); + visuals.widgets.active.corner_radius = t.rounding_sm(); visuals.widgets.open.bg_fill = t.card; visuals.widgets.open.bg_stroke = Stroke::new(1.0, t.border_strong); - visuals.widgets.open.rounding = t.rounding_sm(); + visuals.widgets.open.corner_radius = t.rounding_sm(); visuals.selection.bg_fill = t.accent_soft; visuals.selection.stroke = Stroke::new(1.0, t.accent); - visuals.window_rounding = t.rounding_md(); - visuals.menu_rounding = t.rounding_sm(); + visuals.window_corner_radius = t.rounding_md(); + visuals.menu_corner_radius = t.rounding_sm(); visuals.window_stroke = Stroke::new(1.0, t.border_strong); visuals.window_shadow = egui::epaint::Shadow::NONE; visuals.popup_shadow = egui::epaint::Shadow::NONE; - ctx.set_visuals(visuals); - - let mut style = (*ctx.style()).clone(); - // Named type scale — consume via RichText::text_style(TextStyle::Name(..)). - let proportional = |size: f32| FontId::new(size, FontFamily::Proportional); - style - .text_styles - .insert(TextStyle::Heading, proportional(27.0)); - style - .text_styles - .insert(TextStyle::Name(Arc::from("h2")), proportional(16.0)); - style - .text_styles - .insert(TextStyle::Name(Arc::from("h3")), proportional(13.0)); - style - .text_styles - .insert(TextStyle::Body, proportional(14.0)); - style - .text_styles - .insert(TextStyle::Button, proportional(13.5)); - style - .text_styles - .insert(TextStyle::Small, proportional(12.0)); - style.text_styles.insert( - TextStyle::Monospace, - FontId::new(12.0, FontFamily::Monospace), - ); - - style.spacing.item_spacing = egui::vec2(t.space_3, t.space_3); - style.spacing.button_padding = egui::vec2(t.space_3, t.space_2); - style.spacing.window_margin = egui::Margin::same(t.space_2); - style.spacing.menu_margin = egui::Margin::same(t.space_2); - style.spacing.indent = 18.0; - - ctx.set_style(style); + // egui 0.34+ keeps separate dark/light `Style`s on the `Context` instead + // of one active `Style`. Pin the active theme to what `t` resolved to, + // then fully overwrite that theme's style — preserves the pre-0.34 + // behaviour of `apply()` unconditionally driving the single active style. + let theme = if t.dark { + egui::Theme::Dark + } else { + egui::Theme::Light + }; + ctx.set_theme(theme); + + ctx.style_mut_of(theme, |style| { + style.visuals = visuals; + + // Named type scale — consume via RichText::text_style(TextStyle::Name(..)). + let proportional = |size: f32| FontId::new(size, FontFamily::Proportional); + style + .text_styles + .insert(TextStyle::Heading, proportional(27.0)); + style + .text_styles + .insert(TextStyle::Name(Arc::from("h2")), proportional(16.0)); + style + .text_styles + .insert(TextStyle::Name(Arc::from("h3")), proportional(13.0)); + style + .text_styles + .insert(TextStyle::Body, proportional(14.0)); + style + .text_styles + .insert(TextStyle::Button, proportional(13.5)); + style + .text_styles + .insert(TextStyle::Small, proportional(12.0)); + style.text_styles.insert( + TextStyle::Monospace, + FontId::new(12.0, FontFamily::Monospace), + ); + + style.spacing.item_spacing = egui::vec2(t.space_3, t.space_3); + style.spacing.button_padding = egui::vec2(t.space_3, t.space_2); + style.spacing.window_margin = egui::Margin::same(t.space_2 as i8); + style.spacing.menu_margin = egui::Margin::same(t.space_2 as i8); + style.spacing.indent = 18.0; + }); } diff --git a/src/tokens.rs b/src/tokens.rs index 522a8ce..014ca4e 100644 --- a/src/tokens.rs +++ b/src/tokens.rs @@ -285,18 +285,18 @@ impl Tokens { } } - /// `radius_md` as an [`egui::Rounding`]. - pub fn rounding_md(&self) -> egui::Rounding { - egui::Rounding::same(self.radius_md) + /// `radius_md` as an [`egui::CornerRadius`]. + pub fn rounding_md(&self) -> egui::CornerRadius { + egui::CornerRadius::same(self.radius_md as u8) } - /// `radius_sm` as an [`egui::Rounding`]. - pub fn rounding_sm(&self) -> egui::Rounding { - egui::Rounding::same(self.radius_sm) + /// `radius_sm` as an [`egui::CornerRadius`]. + pub fn rounding_sm(&self) -> egui::CornerRadius { + egui::CornerRadius::same(self.radius_sm as u8) } - /// `radius_xs` as an [`egui::Rounding`]. - pub fn rounding_xs(&self) -> egui::Rounding { - egui::Rounding::same(self.radius_xs) + /// `radius_xs` as an [`egui::CornerRadius`]. + pub fn rounding_xs(&self) -> egui::CornerRadius { + egui::CornerRadius::same(self.radius_xs as u8) } } From 169e4c7d40d3f9a75c7aedc5df94a0b6560dec34 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 19 Aug 2026 04:14:51 +0530 Subject: [PATCH 2/2] Fix review findings: StrokeKind::Outside, MSRV 1.92, winit CSD/dlopen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review of PR #23 caught three real issues before merge: 1. StrokeKind::Inside was the wrong migration choice. Verified directly against the egui 0.29.1 epaint source (tessellator.rs:1709, `PathStroke::from(stroke).outside()` inside `tessellate_rect`) — 0.29's rect strokes were StrokeKind::Outside, not the migration guide's general "you probably want Inside" recommendation. Inside would have shifted every card/button/input border inward by a full stroke width. Flipped all 13 rect/rect_stroke sites in components.rs to Outside. 2. CI was failing on MSRV: egui 0.35 requires rustc 1.92 (confirmed via egui-0.35.0/Cargo.toml `rust-version = "1.92"`), but this repo had no rust-toolchain.toml and CI pinned rustc 1.88 via a literal dtolnay action version. Added rust-toolchain.toml (channel = "stable", mirrors tokito's), bumped Cargo.toml rust-version to 1.92, and repinned ci.yml's toolchain action to the 1.92.0 branch SHA. This also newly unlocks an MSRV-gated clippy suggestion (`map_or(true, ..)` -> `is_none_or(..)`) — fixed the one call site clippy flagged. 3. Fixed the gallery dev-dependency's `eframe` to build on glow instead of 0.35's new wgpu default, matching the consuming app. `winit/default` (Wayland CSD + dlopen) isn't reachable through eframe's feature table from a downstream Cargo.toml (`dep/feature` syntax only reaches a *direct* dependency) — added `winit` as a direct dev-dependency at the same version eframe pins, requesting only the two sub-features (`wayland-csd-adwaita`, `wayland-dlopen`) that eframe's default-off build otherwise drops, restored via Cargo's per-package feature unification. Also fixed a broken intra-doc link (`egui::SidePanel::right` -> `egui::Panel::right`) that `cargo doc -D warnings` caught. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 2 +- Cargo.toml | 23 +++++++++++++++++++++-- rust-toolchain.toml | 3 +++ src/components.rs | 32 ++++++++++++++++---------------- 4 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86e9836..816a890 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: sudo apt-get update sudo apt-get install -y libxkbcommon-dev libgtk-3-dev - - uses: dtolnay/rust-toolchain@98e1b82157cd469e843cb7f524c1313b4ad9492c # 1.88 + - uses: dtolnay/rust-toolchain@87eb139fed4b08a67bd1fa429a21d1f5d523e03e # 1.92.0 with: components: rustfmt, clippy diff --git a/Cargo.toml b/Cargo.toml index 2a1bbc1..c1e67c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "tokito_ui" version = "0.1.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.92" description = "A small, opinionated egui component library — the design layer for Tokito." license = "MIT" repository = "https://github.com/TokitoAI/ui" @@ -17,7 +17,26 @@ egui_extras = "0.35" image = { version = "0.25", default-features = false, features = ["png"] } [dev-dependencies] -eframe = "0.35" +# default-features off: eframe 0.35 flipped its default renderer from glow to +# wgpu; the gallery example renders on glow, matching the consuming app. +eframe = { version = "0.35", default-features = false, features = [ + "accesskit", + "default_fonts", + "glow", + "wayland", + "web_screen_reader", + "x11", +] } +# `winit/default` (Wayland CSD + dlopen) can't be requested through eframe's +# feature table — `dep/feature` syntax only reaches a *direct* dependency's +# features, and winit is eframe's dependency, not ours. Cargo unifies +# features per resolved package version, so declaring winit directly (same +# version eframe pins, unused in code) with just the two sub-features eframe +# drops when its own defaults are off restores them for the gallery example. +winit = { version = "0.30", default-features = false, features = [ + "wayland-csd-adwaita", + "wayland-dlopen", +] } [lints.clippy] too_many_arguments = "allow" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..73cb934 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] diff --git a/src/components.rs b/src/components.rs index 135b38e..4278438 100644 --- a/src/components.rs +++ b/src/components.rs @@ -57,7 +57,7 @@ pub fn card(ui: &mut Ui, t: &Tokens, size: Vec2, add_contents: impl FnOnce(&mut rect.shrink(0.5), t.rounding_md(), Stroke::new(1.0, border), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); let mut content = ui.new_child( @@ -136,7 +136,7 @@ pub fn icon_button(ui: &mut Ui, t: &Tokens, glyph: &str, side: f32, ink: Color32 rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.border.gamma_multiply(hv)), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); } ui.painter().text( @@ -195,7 +195,7 @@ pub fn text_button( rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, b), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); } ui.painter() @@ -228,7 +228,7 @@ pub fn badge(ui: &mut Ui, t: &Tokens, text: &str) -> Response { rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.border), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); ui.painter() .galley(rect.center() - galley.size() / 2.0, galley, t.text_3); @@ -397,7 +397,7 @@ fn bordered_input( rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, border), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); let text_left = if let Some(glyph) = leading_glyph { ui.painter().text( @@ -697,7 +697,7 @@ pub fn checkbox( box_rect.shrink(0.5), t.rounding_xs(), Stroke::new(1.0, border), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); if on > 0.01 { let c = box_rect.center(); @@ -747,7 +747,7 @@ pub fn segmented( rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.border), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); let n = options.len().max(1); @@ -821,7 +821,7 @@ pub fn select( rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, border), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); ui.painter().text( pos2(rect.left() + 11.0, rect.center().y), @@ -946,7 +946,7 @@ pub fn banner( rect.shrink(0.5), t.rounding_md(), Stroke::new(1.0, accent.gamma_multiply(0.55)), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); ui.painter().text( pos2( @@ -1104,7 +1104,7 @@ where }; painter.rect_filled(rect, t.rounding_sm(), fill); - painter.rect_stroke(rect, t.rounding_sm(), stroke, egui::StrokeKind::Inside); + painter.rect_stroke(rect, t.rounding_sm(), stroke, egui::StrokeKind::Outside); let ink = if selected { t.accent @@ -1450,7 +1450,7 @@ impl ToastStack { fn prune(&mut self) { let now = std::time::Instant::now(); - self.items.retain(|t| t.until.map_or(true, |u| u > now)); + self.items.retain(|t| t.until.is_none_or(|u| u > now)); } } @@ -1831,7 +1831,7 @@ fn tab_pill(ui: &mut Ui, t: &Tokens, icon: &str, label: &str, selected: bool) -> rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.accent), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); } @@ -2209,8 +2209,8 @@ pub enum AiHelperRailAction { /// - [`AiHelperRailState::Expanded`] — paints the full rail: header with /// close + collapse, a stack of suggestion chips, and a quick composer. /// -/// The caller is expected to host this in an [`egui::SidePanel::right`] (or -/// equivalent) and adjust the panel's `exact_width` to match the current +/// The caller is expected to host this in an [`egui::Panel::right`] (or +/// equivalent) and adjust the panel's `exact_size` to match the current /// state. pub fn ai_helper_rail( ui: &mut Ui, @@ -2316,7 +2316,7 @@ fn rail_suggestion(ui: &mut Ui, t: &Tokens, label: &str) -> Response { rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, border), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); ui.painter().text( pos2(rect.left() + 12.0, rect.center().y), @@ -2362,7 +2362,7 @@ pub fn thread_row( rect.shrink(0.5), t.rounding_sm(), Stroke::new(1.0, t.accent), - egui::StrokeKind::Inside, + egui::StrokeKind::Outside, ); }