mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Let the theme style submenus, and let a button's fill bleed out of its box
Three things a menu needs and nothing else does: * A submenu asked `Frame::menu` for its frame directly, so it ignored the theme. It now resolves the same `PopupStyle` a `Popup` does — it needs the margin itself, to place the submenu and to decide how much of the parent menu counts as hovered. * `ScrollStyle::overflow_margin` lets a scroll area's contents paint a little outside the viewport, at the ends of the scroll range where nothing could scroll into view through the gap. A menu item whose fill bleeds past its own box needs it; without it the fill is sliced. * A frameless button dropped its inner margin but kept the outer one, so its contents shifted as soon as it was hovered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,13 @@
|
|||||||
//! See [`MenuBar`] for an example.
|
//! See [`MenuBar`] for an example.
|
||||||
|
|
||||||
use crate::style::StyleModifier;
|
use crate::style::StyleModifier;
|
||||||
|
use crate::widget_style::{
|
||||||
|
Classes, HasClasses as _, MENU_CLASS, PopupStyle, StyleArgs, WidgetState,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
|
Button, Color32, Context, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
|
||||||
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
|
PopupCloseBehavior, PopupKind, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo,
|
||||||
|
Widget as _,
|
||||||
};
|
};
|
||||||
use emath::{Align, RectAlign, Vec2, vec2};
|
use emath::{Align, RectAlign, Vec2, vec2};
|
||||||
use epaint::Stroke;
|
use epaint::Stroke;
|
||||||
@@ -429,10 +433,22 @@ impl SubMenu {
|
|||||||
button_response: &Response,
|
button_response: &Response,
|
||||||
content: impl FnOnce(&mut Ui) -> R,
|
content: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<InnerResponse<R>> {
|
) -> Option<InnerResponse<R>> {
|
||||||
let frame = Frame::menu(ui.style());
|
|
||||||
|
|
||||||
let id = Self::id_from_widget_id(button_response.id);
|
let id = Self::id_from_widget_id(button_response.id);
|
||||||
|
|
||||||
|
// A submenu is a menu, so the theme frames it like one. `Popup` computes the same style
|
||||||
|
// itself; we need it here too, because its margin decides where the submenu sits and how
|
||||||
|
// much of the parent menu counts as hovered. A popup frame does not depend on any widget
|
||||||
|
// state, so we ask for it directly rather than through `Ui::widget_style`, which would
|
||||||
|
// want a response that does not exist while the submenu is closed.
|
||||||
|
let classes = Classes::default().with_class(MENU_CLASS);
|
||||||
|
let PopupStyle { frame, .. } = ui.ctx().get_widget_style(&StyleArgs {
|
||||||
|
classes: &classes,
|
||||||
|
state: WidgetState::default(),
|
||||||
|
stack: ui.stack(),
|
||||||
|
style: ui.style(),
|
||||||
|
ctx: ui.ctx(),
|
||||||
|
});
|
||||||
|
|
||||||
// Get the state from the parent menu
|
// Get the state from the parent menu
|
||||||
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
|
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
|
||||||
(state.open_item, stack.id, MenuConfig::from_stack(stack))
|
(state.open_item, stack.id, MenuConfig::from_stack(stack))
|
||||||
@@ -501,12 +517,12 @@ impl SubMenu {
|
|||||||
|
|
||||||
let popup_response = Popup::from_response(&response)
|
let popup_response = Popup::from_response(&response)
|
||||||
.id(id)
|
.id(id)
|
||||||
|
.kind(PopupKind::Menu)
|
||||||
.open(is_open)
|
.open(is_open)
|
||||||
.align(RectAlign::RIGHT_START)
|
.align(RectAlign::RIGHT_START)
|
||||||
.layout(Layout::top_down_justified(Align::Min))
|
.layout(Layout::top_down_justified(Align::Min))
|
||||||
.gap(gap)
|
.gap(gap)
|
||||||
.style(menu_config.style.clone())
|
.style(menu_config.style.clone())
|
||||||
.frame(frame)
|
|
||||||
// The close behavior is handled by the menu (see below)
|
// The close behavior is handled by the menu (see below)
|
||||||
.close_behavior(PopupCloseBehavior::IgnoreClicks)
|
.close_behavior(PopupCloseBehavior::IgnoreClicks)
|
||||||
.info(
|
.info(
|
||||||
|
|||||||
@@ -637,6 +637,7 @@ impl<'a> Popup<'a> {
|
|||||||
// The theme decides how the popup is framed and how tightly its items sit together.
|
// The theme decides how the popup is framed and how tightly its items sit together.
|
||||||
let popup_style: PopupStyle = ui.widget_style(id, &classes);
|
let popup_style: PopupStyle = ui.widget_style(id, &classes);
|
||||||
ui.spacing_mut().item_spacing = popup_style.item_spacing;
|
ui.spacing_mut().item_spacing = popup_style.item_spacing;
|
||||||
|
ui.spacing_mut().scroll.overflow_margin = popup_style.scroll_overflow_margin;
|
||||||
|
|
||||||
let frame = frame.unwrap_or(popup_style.frame);
|
let frame = frame.unwrap_or(popup_style.frame);
|
||||||
frame.show(ui, content).inner
|
frame.show(ui, content).inner
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ pub struct State {
|
|||||||
/// The content were to large to fit large frame.
|
/// The content were to large to fit large frame.
|
||||||
content_is_too_large: Vec2b,
|
content_is_too_large: Vec2b,
|
||||||
|
|
||||||
|
/// The largest offset the content allowed last frame.
|
||||||
|
///
|
||||||
|
/// Zero or less when the content fits, so there is nothing to scroll.
|
||||||
|
max_offset: Vec2,
|
||||||
|
|
||||||
/// Did the user interact (hover or drag) the scroll bars last frame?
|
/// Did the user interact (hover or drag) the scroll bars last frame?
|
||||||
scroll_bar_interaction: Vec2b,
|
scroll_bar_interaction: Vec2b,
|
||||||
|
|
||||||
@@ -62,6 +67,7 @@ impl Default for State {
|
|||||||
offset_target: Default::default(),
|
offset_target: Default::default(),
|
||||||
show_scroll: Vec2b::FALSE,
|
show_scroll: Vec2b::FALSE,
|
||||||
content_is_too_large: Vec2b::FALSE,
|
content_is_too_large: Vec2b::FALSE,
|
||||||
|
max_offset: Vec2::ZERO,
|
||||||
scroll_bar_interaction: Vec2b::FALSE,
|
scroll_bar_interaction: Vec2b::FALSE,
|
||||||
vel: Vec2::ZERO,
|
vel: Vec2::ZERO,
|
||||||
scroll_start_offset_from_top_left: [None; 2],
|
scroll_start_offset_from_top_left: [None; 2],
|
||||||
@@ -353,6 +359,7 @@ pub struct ScrollArea {
|
|||||||
wheel_scroll_multiplier: Vec2,
|
wheel_scroll_multiplier: Vec2,
|
||||||
|
|
||||||
content_margin: Option<Margin>,
|
content_margin: Option<Margin>,
|
||||||
|
overflow_margin: Option<Margin>,
|
||||||
|
|
||||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||||
/// end position until user manually changes position. It will become true
|
/// end position until user manually changes position. It will become true
|
||||||
@@ -407,6 +414,7 @@ impl ScrollArea {
|
|||||||
scroll_source: ScrollSource::default(),
|
scroll_source: ScrollSource::default(),
|
||||||
wheel_scroll_multiplier: Vec2::splat(1.0),
|
wheel_scroll_multiplier: Vec2::splat(1.0),
|
||||||
content_margin: None,
|
content_margin: None,
|
||||||
|
overflow_margin: None,
|
||||||
stick_to_end: Vec2b::FALSE,
|
stick_to_end: Vec2b::FALSE,
|
||||||
animated: true,
|
animated: true,
|
||||||
}
|
}
|
||||||
@@ -633,6 +641,19 @@ impl ScrollArea {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How far the contents may paint outside the viewport.
|
||||||
|
///
|
||||||
|
/// Some widgets paint outside their own box — a menu item whose fill bleeds past it, say — and
|
||||||
|
/// the scroll area would otherwise cut that off. Only the ends of the scroll range are
|
||||||
|
/// widened, where there is nothing that could scroll into view through the gap.
|
||||||
|
///
|
||||||
|
/// Default: [`crate::style::ScrollStyle::overflow_margin`].
|
||||||
|
#[inline]
|
||||||
|
pub fn overflow_margin(mut self, margin: impl Into<Margin>) -> Self {
|
||||||
|
self.overflow_margin = Some(margin.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// The scroll handle will stick to the rightmost position even while the content size
|
/// The scroll handle will stick to the rightmost position even while the content size
|
||||||
/// changes dynamically. This can be useful to simulate text scrollers coming in from right
|
/// changes dynamically. This can be useful to simulate text scrollers coming in from right
|
||||||
/// hand side. The scroll handle remains stuck until user manually changes position. Once "unstuck"
|
/// hand side. The scroll handle remains stuck until user manually changes position. Once "unstuck"
|
||||||
@@ -724,6 +745,7 @@ impl ScrollArea {
|
|||||||
scroll_source,
|
scroll_source,
|
||||||
wheel_scroll_multiplier,
|
wheel_scroll_multiplier,
|
||||||
content_margin: _, // Used elsewhere
|
content_margin: _, // Used elsewhere
|
||||||
|
overflow_margin,
|
||||||
stick_to_end,
|
stick_to_end,
|
||||||
animated,
|
animated,
|
||||||
} = self;
|
} = self;
|
||||||
@@ -811,6 +833,11 @@ impl ScrollArea {
|
|||||||
{
|
{
|
||||||
// Clip the content, but only when we really need to:
|
// Clip the content, but only when we really need to:
|
||||||
let mut content_clip_rect = ui.clip_rect();
|
let mut content_clip_rect = ui.clip_rect();
|
||||||
|
let overflow_margin =
|
||||||
|
overflow_margin.unwrap_or_else(|| ui.spacing().scroll.overflow_margin);
|
||||||
|
let overflow_min = Vec2::new(overflow_margin.leftf(), overflow_margin.topf());
|
||||||
|
let overflow_max = Vec2::new(overflow_margin.rightf(), overflow_margin.bottomf());
|
||||||
|
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if direction_enabled[d] {
|
if direction_enabled[d] {
|
||||||
content_clip_rect.min[d] = inner_rect.min[d];
|
content_clip_rect.min[d] = inner_rect.min[d];
|
||||||
@@ -819,8 +846,18 @@ impl ScrollArea {
|
|||||||
// Nice handling of forced resizing beyond the possible:
|
// Nice handling of forced resizing beyond the possible:
|
||||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Let the contents paint a little outside the viewport, but only where nothing
|
||||||
|
// could scroll into view through the gap: at the start of the scroll range there
|
||||||
|
// is nothing above the first item, and at its end nothing below the last one.
|
||||||
|
if !direction_enabled[d] || state.offset[d] <= 0.0 {
|
||||||
|
content_clip_rect.min[d] -= overflow_min[d];
|
||||||
|
}
|
||||||
|
if !direction_enabled[d] || state.max_offset[d] <= state.offset[d] {
|
||||||
|
content_clip_rect.max[d] += overflow_max[d];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Make sure we didn't accidentally expand the clip rect
|
// Make sure we didn't grow past what our parent allows
|
||||||
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
|
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
|
||||||
content_ui.set_clip_rect(content_clip_rect);
|
content_ui.set_clip_rect(content_clip_rect);
|
||||||
}
|
}
|
||||||
@@ -1551,6 +1588,7 @@ impl Prepared {
|
|||||||
|
|
||||||
state.show_scroll = show_scroll_this_frame;
|
state.show_scroll = show_scroll_this_frame;
|
||||||
state.content_is_too_large = content_is_too_large;
|
state.content_is_too_large = content_is_too_large;
|
||||||
|
state.max_offset = max_offset;
|
||||||
state.interact_rect = Some(inner_rect);
|
state.interact_rect = Some(inner_rect);
|
||||||
|
|
||||||
state.store(ui.ctx(), id);
|
state.store(ui.ctx(), id);
|
||||||
|
|||||||
@@ -508,6 +508,13 @@ pub struct ScrollStyle {
|
|||||||
/// depending on the value of [`Self::floating`].
|
/// depending on the value of [`Self::floating`].
|
||||||
pub content_margin: Margin,
|
pub content_margin: Margin,
|
||||||
|
|
||||||
|
/// How far the contents of a [`crate::ScrollArea`] may paint outside its viewport.
|
||||||
|
///
|
||||||
|
/// Some widgets paint outside their own box — a menu item whose fill bleeds past it, say — and
|
||||||
|
/// a [`crate::ScrollArea`] would otherwise cut that off. Only the ends of the scroll range are
|
||||||
|
/// widened, where there is nothing that could scroll into view through the gap.
|
||||||
|
pub overflow_margin: Margin,
|
||||||
|
|
||||||
/// The width of the scroll bars at it largest.
|
/// The width of the scroll bars at it largest.
|
||||||
pub bar_width: f32,
|
pub bar_width: f32,
|
||||||
|
|
||||||
@@ -594,6 +601,7 @@ impl ScrollStyle {
|
|||||||
Self {
|
Self {
|
||||||
floating: false,
|
floating: false,
|
||||||
content_margin: Margin::ZERO,
|
content_margin: Margin::ZERO,
|
||||||
|
overflow_margin: Margin::ZERO,
|
||||||
bar_width: 6.0,
|
bar_width: 6.0,
|
||||||
handle_min_length: 12.0,
|
handle_min_length: 12.0,
|
||||||
bar_inner_margin: 4.0,
|
bar_inner_margin: 4.0,
|
||||||
@@ -679,6 +687,7 @@ impl ScrollStyle {
|
|||||||
floating,
|
floating,
|
||||||
|
|
||||||
content_margin,
|
content_margin,
|
||||||
|
overflow_margin,
|
||||||
|
|
||||||
bar_width,
|
bar_width,
|
||||||
handle_min_length,
|
handle_min_length,
|
||||||
@@ -710,6 +719,11 @@ impl ScrollStyle {
|
|||||||
content_margin.ui(ui);
|
content_margin.ui(ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Overflow margin:");
|
||||||
|
overflow_margin.ui(ui);
|
||||||
|
});
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add(DragValue::new(bar_width).range(0.0..=32.0));
|
ui.add(DragValue::new(bar_width).range(0.0..=32.0));
|
||||||
ui.label("Full bar width");
|
ui.label("Full bar width");
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ impl StyleProvider<PopupStyle> for DefaultStyle {
|
|||||||
PopupStyle {
|
PopupStyle {
|
||||||
frame: Frame::popup(style),
|
frame: Frame::popup(style),
|
||||||
item_spacing: style.spacing.item_spacing,
|
item_spacing: style.spacing.item_spacing,
|
||||||
|
scroll_overflow_margin: style.spacing.scroll.overflow_margin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use emath::Vec2;
|
|||||||
use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
|
use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Context, Frame, Response, Style, TextStyle, UiStack,
|
Context, Frame, Margin, Response, Style, TextStyle, UiStack,
|
||||||
style::{WidgetVisuals, Widgets},
|
style::{WidgetVisuals, Widgets},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,6 +109,13 @@ pub struct PopupStyle {
|
|||||||
/// A menu wants its items flush against each other, while a tooltip wants them spaced out
|
/// A menu wants its items flush against each other, while a tooltip wants them spaced out
|
||||||
/// like any other content.
|
/// like any other content.
|
||||||
pub item_spacing: Vec2,
|
pub item_spacing: Vec2,
|
||||||
|
|
||||||
|
/// How far the contents of a [`crate::ScrollArea`] inside the popup may paint outside it.
|
||||||
|
///
|
||||||
|
/// A [`crate::ScrollArea`] clips to its viewport, which would cut off anything a widget paints
|
||||||
|
/// outside its own box — an item whose fill bleeds past it, say. This is how much room such a
|
||||||
|
/// widget gets, applied via [`crate::style::ScrollStyle::overflow_margin`].
|
||||||
|
pub scroll_overflow_margin: Margin,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetStyle for PopupStyle {}
|
impl WidgetStyle for PopupStyle {}
|
||||||
|
|||||||
@@ -366,6 +366,12 @@ impl<'a> Button<'a> {
|
|||||||
|
|
||||||
frame = frame.inner_margin(button_padding);
|
frame = frame.inner_margin(button_padding);
|
||||||
|
|
||||||
|
if !has_frame_margin {
|
||||||
|
// A frameless button asks for no padding, so any outer margin the theme used to
|
||||||
|
// compensate for that padding has to go as well.
|
||||||
|
frame = frame.outer_margin(Margin::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the style font and color as fallback
|
// Apply the style font and color as fallback
|
||||||
layout = layout
|
layout = layout
|
||||||
.fallback_font(text_style.font_id.clone())
|
.fallback_font(text_style.font_id.clone())
|
||||||
@@ -375,7 +381,13 @@ impl<'a> Button<'a> {
|
|||||||
layout = if has_frame_margin && (state != WidgetState::Inactive || frame_when_inactive) {
|
layout = if has_frame_margin && (state != WidgetState::Inactive || frame_when_inactive) {
|
||||||
layout.frame(frame)
|
layout.frame(frame)
|
||||||
} else {
|
} else {
|
||||||
layout.frame(Frame::new().inner_margin(frame.inner_margin))
|
// The frame is not painted, but it still takes up the same space: keep both margins,
|
||||||
|
// or the contents would move as soon as the button is hovered or selected.
|
||||||
|
layout.frame(
|
||||||
|
Frame::new()
|
||||||
|
.inner_margin(frame.inner_margin)
|
||||||
|
.outer_margin(frame.outer_margin),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut prepared = layout.min_size(min_size).allocate(ui);
|
let mut prepared = layout.min_size(min_size).allocate(ui);
|
||||||
|
|||||||
Reference in New Issue
Block a user