You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
impl eframe::App for MyApp {
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
// Match your theme but force full opacity:
let c32 = visuals.window_fill().to_opaque(); // Color32 (alpha = 255)
let rgba: egui::Rgba = c32.into();
rgba.to_array()
// or just: [0.09, 0.09, 0.09, 1.0]
}
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let title_h = 32.0; // a bit taller for easier grabbing
let edge = 6.0; // edges/corners band thickness inside titlebar
// ----------------- TITLEBAR -----------------
egui::TopBottomPanel::top("titlebar")
.frame(egui::Frame::NONE.fill(ctx.style().visuals.window_fill()))
.show(ctx, |ui| {
// Allocate a draggable strip
let (bar_rect, bar_resp) = ui.allocate_at_least(
egui::vec2(ui.available_width(), title_h),
egui::Sense::click_and_drag(), // <-- important
);
// Controls rect (right side)
let mut controls_rect = bar_rect;
let btn_w = 34.0;
let btn_h = 22.0;
let controls_w = 3.0 * btn_w;
controls_rect.set_left(controls_rect.right() - controls_w);
// Title rect (rest of the bar)
let mut title_rect = bar_rect;
title_rect.set_right(controls_rect.left());
// Title text
ui.painter().text(
title_rect.left_center() + egui::vec2(10.0, 0.0),
egui::Align2::LEFT_CENTER,
"Custom Titlebar",
egui::TextStyle::Body.resolve(ui.style()),
ui.visuals().text_color(),
);
// Buttons — use actual `Button` widgets (most robust for hit-testing)
let mut over_controls = false;
ui.allocate_ui_at_rect(controls_rect, |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let mut make_btn = |ui: &mut egui::Ui, label: &str| -> egui::Response {
let r = egui::Button::new(label).min_size(egui::vec2(btn_w, btn_h));
ui.add(r)
};
// Close
let close = make_btn(ui, "×");
if close.hovered() || close.is_pointer_button_down_on() { over_controls = true; }
if close.clicked() {
ctx.send_viewport_cmd(ViewportCommand::Close);
}
// Max / Restore
let is_max = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
let max_label = if is_max { "❐" } else { "□" };
let max = make_btn(ui, max_label);
if max.hovered() || max.is_pointer_button_down_on() { over_controls = true; }
if max.clicked() {
ctx.send_viewport_cmd(ViewportCommand::Maximized(!is_max));
}
// Minimize
let min = make_btn(ui, "–");
if min.hovered() || min.is_pointer_button_down_on() { over_controls = true; }
if min.clicked() {
ctx.send_viewport_cmd(ViewportCommand::Minimized(true));
}
});
});
// -------- Titlebar interactions: drag + top-edge/corners resize -------
let is_max = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
let pointer_pos = ctx.input(|i| i.pointer.latest_pos());
let on_bar = pointer_pos.map_or(false, |p| bar_rect.contains(p));
let on_controls = on_bar && controls_rect.contains(pointer_pos.unwrap());
let in_top_band = on_bar && pointer_pos.unwrap().y <= bar_rect.top() + edge;
if !on_controls {
if in_top_band && !is_max {
// Corners first
let x = pointer_pos.unwrap().x;
let near_left = x <= bar_rect.left() + edge;
let near_right = x >= bar_rect.right() - edge;
if near_left {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeNwSe);
if bar_resp.is_pointer_button_down_on() {
ctx.send_viewport_cmd(ViewportCommand::BeginResize(ResizeDirection::NorthWest));
}
} else if near_right {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeNeSw);
if bar_resp.is_pointer_button_down_on() {
ctx.send_viewport_cmd(ViewportCommand::BeginResize(ResizeDirection::NorthEast));
}
} else {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
if bar_resp.is_pointer_button_down_on() {
ctx.send_viewport_cmd(ViewportCommand::BeginResize(ResizeDirection::North));
}
}
} else {
// Window move + double-click maximize
if bar_resp.double_clicked() {
ctx.send_viewport_cmd(ViewportCommand::Maximized(!is_max));
} else if bar_resp.is_pointer_button_down_on() && !is_max {
ctx.send_viewport_cmd(ViewportCommand::StartDrag);
}
}
}
});
// ----------------- CONTENT -----------------
egui::CentralPanel::default()
.frame(egui::Frame::NONE.fill(ctx.style().visuals.window_fill()))
.show(ctx, |ui| {
ui.label("Your app content here");
ui.separator();
ui.label("If the titlebar still doesn't react, comment out the CentralPanel to rule out overlap.");
});
// ----------------- NOTE -----------------
// We intentionally do NOT install any background edge-handles here.
// Once the titlebar & buttons are confirmed interactive, we can add left/right/bottom
// edges back *carefully* so they don't pre-empt pointer input above them.
edge_resize_handles(ctx, 6.0, title_h);
}
}
fn edge_resize_handles(ctx: &egui::Context, edge: f32, title_h: f32) {
use egui::{pos2, vec2, Area, Id, Order, Sense, CursorIcon};
use egui::viewport::{ResizeDirection, ViewportCommand};
let is_max = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
if is_max { return; }
let screen = ctx.input(|i| i.screen_rect());
// Interior body height (between titlebar and bottom band)
let body_h = (screen.height() - title_h - edge).max(0.0);
// Left/right strips: start under titlebar, stop BEFORE bottom band.
let left = egui::Rect::from_min_size(pos2(screen.left(), title_h), vec2(edge, body_h));
let right = egui::Rect::from_min_size(pos2(screen.right() - edge, title_h), vec2(edge, body_h));
// Bottom band: span width but leave gaps equal to `edge` on both sides for corner squares.
let bottom = egui::Rect::from_min_size(
pos2(screen.left() + edge, screen.bottom() - edge),
vec2((screen.width() - 2.0 * edge).max(0.0), edge),
);
// Corner squares (same size as `edge`).
let sw = egui::Rect::from_min_size(pos2(screen.left(), screen.bottom() - edge), vec2(edge, edge));
let se = egui::Rect::from_min_size(pos2(screen.right() - edge, screen.bottom() - edge), vec2(edge, edge));
let mut make_handle = |id: &str, rect: egui::Rect, cursor: CursorIcon, begin: ResizeDirection| {
if !rect.is_positive() { return; }
Area::new(Id::new(id))
.order(Order::Foreground) // above panels
.interactable(true)
.fixed_pos(rect.min)
.show(ctx, |ui| {
let (_r, resp) = ui.allocate_exact_size(rect.size(), Sense::click_and_drag());
if resp.hovered() { ui.output_mut(|o| o.cursor_icon = cursor); }
if resp.is_pointer_button_down_on() {
ctx.send_viewport_cmd(ViewportCommand::BeginResize(begin));
}
});
};
// IMPORTANT: add non-corner edges first, then corners LAST so they win overlap.
make_handle("resize_left", left, CursorIcon::ResizeHorizontal, ResizeDirection::West);
make_handle("resize_right", right, CursorIcon::ResizeHorizontal, ResizeDirection::East);
make_handle("resize_bottom", bottom, CursorIcon::ResizeVertical, ResizeDirection::South);
// Corners last (higher hit priority due to later addition in same layer):
make_handle("resize_sw", sw, CursorIcon::ResizeNeSw, ResizeDirection::SouthWest);
make_handle("resize_se", se, CursorIcon::ResizeNwSe, ResizeDirection::SouthEast);
}
`
when i start the program and resize the window there is some ghosting / artifacting but if i start the program then maximize the window then click maximize again to restore window and then we resize window it works fine no ghosting / artifacting.
i also tried to add corner radius to the window and made it work but when i maximize window then restore it there was some black filling(changed the color to red so its easier to see) where it should be transparent.
`
use eframe::egui;
use egui::viewport::{ResizeDirection, ViewportCommand};
impl eframe::App for MyApp {
// Make the OS clear to transparent so our own rounded bg shows through:
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
egui::Rgba::TRANSPARENT.to_array()
}
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let title_h = 32.0;
let edge = 6.0;
// --- decide whether to show corner radius ---
let vp = ctx.input(|i| i.viewport().clone());
let cornerless = vp.maximized.unwrap_or(false) || vp.fullscreen.unwrap_or(false);
let radius = if cornerless {
egui::CornerRadius::ZERO
} else {
egui::CornerRadius::same(14) // your normal radius
};
let bg = ctx.style().visuals.window_fill();
let stroke = if cornerless {
egui::Stroke::NONE // avoid a visible inner border when maximized
} else {
ctx.style().visuals.window_stroke()
};
// --- Paint ONE rounded background behind everything ---
let screen = ctx.input(|i| i.screen_rect());
let painter = ctx.layer_painter(egui::LayerId::background());
painter.rect_filled(screen, radius, bg);
painter.add(egui::Shape::rect_stroke(
screen,
radius,
stroke,
egui::StrokeKind::Inside,
));
// Make panels transparent (no full-rect fills) ===
egui::TopBottomPanel::top("titlebar")
.frame(egui::Frame::new().fill(egui::Color32::TRANSPARENT))
.show(ctx, |ui| {
// --- your titlebar code unchanged ---
let (bar_rect, bar_resp) = ui.allocate_at_least(
egui::vec2(ui.available_width(), title_h),
egui::Sense::click_and_drag(),
);
let mut controls_rect = bar_rect;
let btn_w = 34.0;
let btn_h = 22.0;
let controls_w = 3.0 * btn_w;
controls_rect.set_left(controls_rect.right() - controls_w);
let mut title_rect = bar_rect;
title_rect.set_right(controls_rect.left());
ui.painter().text(
title_rect.left_center() + egui::vec2(10.0, 0.0),
egui::Align2::LEFT_CENTER,
"Custom Titlebar",
egui::TextStyle::Body.resolve(ui.style()),
ui.visuals().text_color(),
);
let mut over_controls = false;
ui.allocate_ui_at_rect(controls_rect, |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let mut make_btn = |ui: &mut egui::Ui, label: &str| -> egui::Response {
let r = egui::Button::new(label).min_size(egui::vec2(btn_w, btn_h));
ui.add(r)
};
let close = make_btn(ui, "×");
if close.hovered() || close.is_pointer_button_down_on() { over_controls = true; }
if close.clicked() { ctx.send_viewport_cmd(ViewportCommand::Close); }
let is_max = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
let max_label = if is_max { "❐" } else { "□" };
let max = make_btn(ui, max_label);
if max.hovered() || max.is_pointer_button_down_on() { over_controls = true; }
if max.clicked() { ctx.send_viewport_cmd(ViewportCommand::Maximized(!is_max)); }
let min = make_btn(ui, "–");
if min.hovered() || min.is_pointer_button_down_on() { over_controls = true; }
if min.clicked() { ctx.send_viewport_cmd(ViewportCommand::Minimized(true)); }
});
});
// --- same drag/resize logic as you had ---
let is_max = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
let pointer_pos = ctx.input(|i| i.pointer.latest_pos());
let on_bar = pointer_pos.map_or(false, |p| bar_rect.contains(p));
let on_controls = on_bar && controls_rect.contains(pointer_pos.unwrap());
let in_top_band = on_bar && pointer_pos.unwrap().y <= bar_rect.top() + edge;
if !on_controls {
if in_top_band && !is_max {
let x = pointer_pos.unwrap().x;
let near_left = x <= bar_rect.left() + edge;
let near_right = x >= bar_rect.right() - edge;
if near_left {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeNwSe);
if bar_resp.is_pointer_button_down_on() {
ctx.send_viewport_cmd(ViewportCommand::BeginResize(ResizeDirection::NorthWest));
}
} else if near_right {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeNeSw);
if bar_resp.is_pointer_button_down_on() {
ctx.send_viewport_cmd(ViewportCommand::BeginResize(ResizeDirection::NorthEast));
}
} else {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
if bar_resp.is_pointer_button_down_on() {
ctx.send_viewport_cmd(ViewportCommand::BeginResize(ResizeDirection::North));
}
}
} else {
if bar_resp.double_clicked() {
ctx.send_viewport_cmd(ViewportCommand::Maximized(!is_max));
} else if bar_resp.is_pointer_button_down_on() && !is_max {
ctx.send_viewport_cmd(ViewportCommand::StartDrag);
}
}
}
});
egui::CentralPanel::default()
.frame(egui::Frame::new().fill(egui::Color32::TRANSPARENT))
.show(ctx, |ui| {
ui.label("Your app content here");
ui.separator();
ui.label("If the titlebar still doesn't react, comment out the CentralPanel to rule out overlap.");
});
edge_resize_handles(ctx, 6.0, title_h);
}
}
fn edge_resize_handles(ctx: &egui::Context, edge: f32, title_h: f32) {
use egui::{pos2, vec2, Area, Id, Order, Sense, CursorIcon};
use egui::viewport::{ResizeDirection, ViewportCommand};
let is_max = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
if is_max { return; }
let screen = ctx.input(|i| i.screen_rect());
// Interior body height (between titlebar and bottom band)
let body_h = (screen.height() - title_h - edge).max(0.0);
// Left/right strips: start under titlebar, stop BEFORE bottom band.
let left = egui::Rect::from_min_size(pos2(screen.left(), title_h), vec2(edge, body_h));
let right = egui::Rect::from_min_size(pos2(screen.right() - edge, title_h), vec2(edge, body_h));
// Bottom band: span width but leave gaps equal to `edge` on both sides for corner squares.
let bottom = egui::Rect::from_min_size(
pos2(screen.left() + edge, screen.bottom() - edge),
vec2((screen.width() - 2.0 * edge).max(0.0), edge),
);
// Corner squares (same size as `edge`).
let sw = egui::Rect::from_min_size(pos2(screen.left(), screen.bottom() - edge), vec2(edge, edge));
let se = egui::Rect::from_min_size(pos2(screen.right() - edge, screen.bottom() - edge), vec2(edge, edge));
let mut make_handle = |id: &str, rect: egui::Rect, cursor: CursorIcon, begin: ResizeDirection| {
if !rect.is_positive() { return; }
Area::new(Id::new(id))
.order(Order::Foreground) // above panels
.interactable(true)
.fixed_pos(rect.min)
.show(ctx, |ui| {
let (_r, resp) = ui.allocate_exact_size(rect.size(), Sense::click_and_drag());
if resp.hovered() { ui.output_mut(|o| o.cursor_icon = cursor); }
if resp.is_pointer_button_down_on() {
ctx.send_viewport_cmd(ViewportCommand::BeginResize(begin));
}
});
};
// IMPORTANT: add non-corner edges first, then corners LAST so they win overlap.
make_handle("resize_left", left, CursorIcon::ResizeHorizontal, ResizeDirection::West);
make_handle("resize_right", right, CursorIcon::ResizeHorizontal, ResizeDirection::East);
make_handle("resize_bottom", bottom, CursorIcon::ResizeVertical, ResizeDirection::South);
// Corners last (higher hit priority due to later addition in same layer):
make_handle("resize_sw", sw, CursorIcon::ResizeNeSw, ResizeDirection::SouthWest);
make_handle("resize_se", se, CursorIcon::ResizeNwSe, ResizeDirection::SouthEast);
}
`
i added the red color to the corners so its easier to see.
i need help with both of these.
im on windows 11.
and my cargo.toml is "[package]
name = "test"
version = "0.1.0"
edition = "2024"
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
so i wanted to have a custom titlebar for my application and i got it to work but there is a few things i need help with.
somewhat working code [i updated the code at the bottom of post]:
`
use eframe::egui;
use egui::viewport::{ResizeDirection, ViewportCommand};
fn main() -> eframe::Result<()> {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_decorations(false)
.with_resizable(true)
.with_transparent(false),
..Default::default()
};
}
#[derive(Default)]
struct MyApp;
impl eframe::App for MyApp {
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
// Match your theme but force full opacity:
let c32 = visuals.window_fill().to_opaque(); // Color32 (alpha = 255)
let rgba: egui::Rgba = c32.into();
rgba.to_array()
// or just: [0.09, 0.09, 0.09, 1.0]
}
}
fn edge_resize_handles(ctx: &egui::Context, edge: f32, title_h: f32) {
use egui::{pos2, vec2, Area, Id, Order, Sense, CursorIcon};
use egui::viewport::{ResizeDirection, ViewportCommand};
}
`
when i start the program and resize the window there is some ghosting / artifacting but if i start the program then maximize the window then click maximize again to restore window and then we resize window it works fine no ghosting / artifacting.
i also tried to add corner radius to the window and made it work but when i maximize window then restore it there was some black filling(changed the color to red so its easier to see) where it should be transparent.
`
use eframe::egui;
use egui::viewport::{ResizeDirection, ViewportCommand};
fn main() -> eframe::Result<()> {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_decorations(false) // custom chrome
.with_resizable(true)
.with_transparent(true), // ← needed for “real” cut-out corners
..Default::default()
};
}
#[derive(Default)]
struct MyApp;
impl eframe::App for MyApp {
// Make the OS clear to transparent so our own rounded bg shows through:
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
egui::Rgba::TRANSPARENT.to_array()
}
}
fn edge_resize_handles(ctx: &egui::Context, edge: f32, title_h: f32) {
use egui::{pos2, vec2, Area, Id, Order, Sense, CursorIcon};
use egui::viewport::{ResizeDirection, ViewportCommand};
}
`
i added the red color to the corners so its easier to see.

i need help with both of these.
im on windows 11.
and my cargo.toml is "[package]
name = "test"
version = "0.1.0"
edition = "2024"
[dependencies]
eframe = "0.32.1"
egui = "0.32.1"
"
[Update]
i made some changes to the code:
`
use eframe::egui;
use egui::viewport::{ResizeDirection, ViewportCommand};
fn main() -> eframe::Result<()> {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_decorations(false)
.with_resizable(true)
.with_transparent(false)
.with_inner_size(egui::vec2(960.0, 600.0)),
..Default::default()
};
}
#[derive(Default)]
struct App {
titlebar_height: f32,
is_maximized: bool,
}
impl App {
fn new(cc: &eframe::CreationContext<'>) -> Self {
Self {
titlebar_height: 32.0,
is_maximized: false,
}
}
}
impl eframe::App for App {
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
Self::clear_rgba(visuals)
}
}
`
please let me know if there is anything i can do or should do differently. (i did use some ai when trying to test and fix this since i am new to rust)
Beta Was this translation helpful? Give feedback.
All reactions