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.
|
||||
|
||||
use crate::style::StyleModifier;
|
||||
use crate::widget_style::{
|
||||
Classes, HasClasses as _, MENU_CLASS, PopupStyle, StyleArgs, WidgetState,
|
||||
};
|
||||
use crate::{
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
|
||||
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
|
||||
Button, Color32, Context, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
|
||||
PopupCloseBehavior, PopupKind, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo,
|
||||
Widget as _,
|
||||
};
|
||||
use emath::{Align, RectAlign, Vec2, vec2};
|
||||
use epaint::Stroke;
|
||||
@@ -429,10 +433,22 @@ impl SubMenu {
|
||||
button_response: &Response,
|
||||
content: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let frame = Frame::menu(ui.style());
|
||||
|
||||
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
|
||||
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
|
||||
(state.open_item, stack.id, MenuConfig::from_stack(stack))
|
||||
@@ -501,12 +517,12 @@ impl SubMenu {
|
||||
|
||||
let popup_response = Popup::from_response(&response)
|
||||
.id(id)
|
||||
.kind(PopupKind::Menu)
|
||||
.open(is_open)
|
||||
.align(RectAlign::RIGHT_START)
|
||||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.gap(gap)
|
||||
.style(menu_config.style.clone())
|
||||
.frame(frame)
|
||||
// The close behavior is handled by the menu (see below)
|
||||
.close_behavior(PopupCloseBehavior::IgnoreClicks)
|
||||
.info(
|
||||
|
||||
@@ -637,6 +637,7 @@ impl<'a> Popup<'a> {
|
||||
// The theme decides how the popup is framed and how tightly its items sit together.
|
||||
let popup_style: PopupStyle = ui.widget_style(id, &classes);
|
||||
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);
|
||||
frame.show(ui, content).inner
|
||||
|
||||
@@ -36,6 +36,11 @@ pub struct State {
|
||||
/// The content were to large to fit large frame.
|
||||
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?
|
||||
scroll_bar_interaction: Vec2b,
|
||||
|
||||
@@ -62,6 +67,7 @@ impl Default for State {
|
||||
offset_target: Default::default(),
|
||||
show_scroll: Vec2b::FALSE,
|
||||
content_is_too_large: Vec2b::FALSE,
|
||||
max_offset: Vec2::ZERO,
|
||||
scroll_bar_interaction: Vec2b::FALSE,
|
||||
vel: Vec2::ZERO,
|
||||
scroll_start_offset_from_top_left: [None; 2],
|
||||
@@ -353,6 +359,7 @@ pub struct ScrollArea {
|
||||
wheel_scroll_multiplier: Vec2,
|
||||
|
||||
content_margin: Option<Margin>,
|
||||
overflow_margin: Option<Margin>,
|
||||
|
||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||
/// end position until user manually changes position. It will become true
|
||||
@@ -407,6 +414,7 @@ impl ScrollArea {
|
||||
scroll_source: ScrollSource::default(),
|
||||
wheel_scroll_multiplier: Vec2::splat(1.0),
|
||||
content_margin: None,
|
||||
overflow_margin: None,
|
||||
stick_to_end: Vec2b::FALSE,
|
||||
animated: true,
|
||||
}
|
||||
@@ -633,6 +641,19 @@ impl ScrollArea {
|
||||
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
|
||||
/// 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"
|
||||
@@ -724,6 +745,7 @@ impl ScrollArea {
|
||||
scroll_source,
|
||||
wheel_scroll_multiplier,
|
||||
content_margin: _, // Used elsewhere
|
||||
overflow_margin,
|
||||
stick_to_end,
|
||||
animated,
|
||||
} = self;
|
||||
@@ -811,6 +833,11 @@ impl ScrollArea {
|
||||
{
|
||||
// Clip the content, but only when we really need to:
|
||||
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 {
|
||||
if direction_enabled[d] {
|
||||
content_clip_rect.min[d] = inner_rect.min[d];
|
||||
@@ -819,8 +846,18 @@ impl ScrollArea {
|
||||
// Nice handling of forced resizing beyond the possible:
|
||||
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_ui.set_clip_rect(content_clip_rect);
|
||||
}
|
||||
@@ -1551,6 +1588,7 @@ impl Prepared {
|
||||
|
||||
state.show_scroll = show_scroll_this_frame;
|
||||
state.content_is_too_large = content_is_too_large;
|
||||
state.max_offset = max_offset;
|
||||
state.interact_rect = Some(inner_rect);
|
||||
|
||||
state.store(ui.ctx(), id);
|
||||
|
||||
@@ -508,6 +508,13 @@ pub struct ScrollStyle {
|
||||
/// depending on the value of [`Self::floating`].
|
||||
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.
|
||||
pub bar_width: f32,
|
||||
|
||||
@@ -594,6 +601,7 @@ impl ScrollStyle {
|
||||
Self {
|
||||
floating: false,
|
||||
content_margin: Margin::ZERO,
|
||||
overflow_margin: Margin::ZERO,
|
||||
bar_width: 6.0,
|
||||
handle_min_length: 12.0,
|
||||
bar_inner_margin: 4.0,
|
||||
@@ -679,6 +687,7 @@ impl ScrollStyle {
|
||||
floating,
|
||||
|
||||
content_margin,
|
||||
overflow_margin,
|
||||
|
||||
bar_width,
|
||||
handle_min_length,
|
||||
@@ -710,6 +719,11 @@ impl ScrollStyle {
|
||||
content_margin.ui(ui);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Overflow margin:");
|
||||
overflow_margin.ui(ui);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(bar_width).range(0.0..=32.0));
|
||||
ui.label("Full bar width");
|
||||
|
||||
@@ -106,6 +106,7 @@ impl StyleProvider<PopupStyle> for DefaultStyle {
|
||||
PopupStyle {
|
||||
frame: Frame::popup(style),
|
||||
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 crate::{
|
||||
Context, Frame, Response, Style, TextStyle, UiStack,
|
||||
Context, Frame, Margin, Response, Style, TextStyle, UiStack,
|
||||
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
|
||||
/// like any other content.
|
||||
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 {}
|
||||
|
||||
@@ -366,6 +366,12 @@ impl<'a> Button<'a> {
|
||||
|
||||
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
|
||||
layout = layout
|
||||
.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.frame(frame)
|
||||
} 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);
|
||||
|
||||
Reference in New Issue
Block a user