Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/egui/src/containers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub(crate) mod window;

pub use {
area::Area,
collapsing_header::{CollapsingHeader, CollapsingResponse},
collapsing_header::{CollapsingHeader, CollapsingResponse, CollapsingState},
combo_box::*,
frame::Frame,
panel::{CentralPanel, SidePanel, TopBottomPanel},
Expand Down
38 changes: 30 additions & 8 deletions crates/egui/src/containers/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pub struct SidePanel {
show_separator_line: bool,
default_width: f32,
width_range: Rangef,
visuals: Option<style::Widgets>,
}

impl SidePanel {
Expand All @@ -121,6 +122,7 @@ impl SidePanel {
show_separator_line: true,
default_width: 200.0,
width_range: Rangef::new(96.0, f32::INFINITY),
visuals: None,
}
}

Expand Down Expand Up @@ -190,6 +192,12 @@ impl SidePanel {
self.frame = Some(frame);
self
}

/// Optionally override visual style
pub fn visuals(mut self, visuals: style::Widgets) -> Self {
self.visuals = Some(visuals);
self
}
}

impl SidePanel {
Expand All @@ -216,6 +224,7 @@ impl SidePanel {
show_separator_line,
default_width,
width_range,
visuals,
} = self;

let available_rect = ui.available_rect_before_wrap();
Expand Down Expand Up @@ -296,15 +305,28 @@ impl SidePanel {
PanelState { rect }.store(ui.ctx(), id);

{
let stroke = if is_resizing {
ui.style().visuals.widgets.active.fg_stroke // highly visible
} else if resize_hover {
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
} else if show_separator_line {
// TODO(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
let stroke = if let Some(widgets) = visuals {
if is_resizing {
widgets.active.fg_stroke // highly visible
} else if resize_hover {
widgets.hovered.fg_stroke // highly visible
} else if show_separator_line {
// TODO(emilk): distinguish resizable from non-resizable
widgets.noninteractive.bg_stroke // dim
} else {
Stroke::NONE
}
} else {
Stroke::NONE
if is_resizing {
ui.style().visuals.widgets.active.fg_stroke // highly visible
} else if resize_hover {
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
} else if show_separator_line {
// TODO(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
} else {
Stroke::NONE
}
};
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
// In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel
Expand Down
32 changes: 31 additions & 1 deletion crates/egui/src/containers/scroll_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ pub struct ScrollArea {
/// end position until user manually changes position. It will become true
/// again once scroll handle makes contact with end.
stick_to_end: [bool; 2],

/// Override for scroll delta. Normally taken from frame_state
override_scroll_delta: Option<Vec2>,
}

impl ScrollArea {
pub fn is_scrolling(ui: &Ui, id_source: Id) -> bool {
let id = ui.make_persistent_id(id_source);
let scroll_target = ui.ctx().frame_state(|state| {
state.scroll_target[0].is_some() || state.scroll_target[1].is_some()
});
if let Some(state) = State::load(ui.ctx(), id) {
state.vel != Vec2::ZERO || scroll_target
} else {
false
}
}
}

impl ScrollArea {
Expand Down Expand Up @@ -158,6 +175,7 @@ impl ScrollArea {
scrolling_enabled: true,
drag_to_scroll: true,
stick_to_end: [false; 2],
override_scroll_delta: None,
}
}

Expand Down Expand Up @@ -332,6 +350,11 @@ impl ScrollArea {
self.stick_to_end[1] = stick;
self
}

pub fn override_scroll_delta(mut self, delta: Vec2) -> Self {
self.override_scroll_delta = Some(delta);
self
}
}

struct Prepared {
Expand All @@ -357,6 +380,7 @@ struct Prepared {

scrolling_enabled: bool,
stick_to_end: [bool; 2],
override_scroll_delta: Option<Vec2>,
}

impl ScrollArea {
Expand All @@ -373,6 +397,7 @@ impl ScrollArea {
scrolling_enabled,
drag_to_scroll,
stick_to_end,
override_scroll_delta,
} = self;

let ctx = ui.ctx().clone();
Expand Down Expand Up @@ -519,6 +544,7 @@ impl ScrollArea {
viewport,
scrolling_enabled,
stick_to_end,
override_scroll_delta,
}
}

Expand Down Expand Up @@ -629,6 +655,7 @@ impl Prepared {
viewport: _,
scrolling_enabled,
stick_to_end,
override_scroll_delta,
} = self;

let content_size = content_ui.min_size();
Expand Down Expand Up @@ -702,7 +729,10 @@ impl Prepared {
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
for d in 0..2 {
if has_bar[d] {
let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
let scroll_delta = match override_scroll_delta {
Some(delta) => delta,
None => ui.ctx().frame_state(|fs| fs.scroll_delta),
};

let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
Expand Down
43 changes: 24 additions & 19 deletions crates/egui/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,35 +704,37 @@ impl Layout {
item_spacing: Vec2,
) {
egui_assert!(!cursor.any_nan());
let mut newline = false;
if self.main_wrap {
if cursor.intersects(frame_rect.shrink(1.0)) {
// make row/column larger if necessary
*cursor = cursor.union(frame_rect);
} else {
// this is a new row or column. We temporarily use NAN for what will be filled in later.
// this is a new row or column. the cursor moves to the edge of the frame
newline = true;
match self.main_dir {
Direction::LeftToRight => {
*cursor = Rect::from_min_max(
pos2(f32::NAN, frame_rect.min.y),
pos2(widget_rect.max.x, frame_rect.min.y),
pos2(INFINITY, frame_rect.max.y),
);
}
Direction::RightToLeft => {
*cursor = Rect::from_min_max(
pos2(-INFINITY, frame_rect.min.y),
pos2(f32::NAN, frame_rect.max.y),
pos2(widget_rect.min.x, frame_rect.max.y),
);
}
Direction::TopDown => {
*cursor = Rect::from_min_max(
pos2(frame_rect.min.x, f32::NAN),
pos2(frame_rect.min.x, widget_rect.max.y),
pos2(frame_rect.max.x, INFINITY),
);
}
Direction::BottomUp => {
*cursor = Rect::from_min_max(
pos2(frame_rect.min.x, -INFINITY),
pos2(frame_rect.max.x, f32::NAN),
pos2(frame_rect.max.x, widget_rect.min.y),
);
}
};
Expand All @@ -748,20 +750,23 @@ impl Layout {
}
}

match self.main_dir {
Direction::LeftToRight => {
cursor.min.x = widget_rect.max.x + item_spacing.x;
}
Direction::RightToLeft => {
cursor.max.x = widget_rect.min.x - item_spacing.x;
}
Direction::TopDown => {
cursor.min.y = widget_rect.max.y + item_spacing.y;
}
Direction::BottomUp => {
cursor.max.y = widget_rect.min.y - item_spacing.y;
}
};
// apply item_spacing unless this is a newline
if !newline {
match self.main_dir {
Direction::LeftToRight => {
cursor.min.x = widget_rect.max.x + item_spacing.x;
}
Direction::RightToLeft => {
cursor.max.x = widget_rect.min.x - item_spacing.x;
}
Direction::TopDown => {
cursor.min.y = widget_rect.max.y + item_spacing.y;
}
Direction::BottomUp => {
cursor.max.y = widget_rect.min.y - item_spacing.y;
}
};
}
}

/// Move to the next row in a wrapping layout.
Expand Down
10 changes: 10 additions & 0 deletions crates/egui/src/widgets/label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ impl Label {
!text_galley.galley.rows.is_empty(),
"Galleys are never empty"
);

// set the row height to ensure the cursor advancement is correct. when creating a child ui such as with
// ui.horizontal_wrapped, the initial cursor will be set to the height of the child ui. this can lead
// to the cursor not advancing to the second row but rather expanding the height of the cursor.
//
// note that we do not set the row height earlier in this function as we do want to allow populating
// `first_row_min_height` above. however it is crucial the placer knows the actual row height by
// setting the cursor height before ui.allocate_rect() gets called.
ui.set_row_height(text_galley.galley.rows[0].height());

// collect a response from many rows:
let rect = text_galley.galley.rows[0]
.rect
Expand Down
75 changes: 52 additions & 23 deletions crates/epaint/src/text/text_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ fn layout_section(
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
}

// TODO(bu5hm4nn): in a label widget, `leading_space` is used to adjust for existing text in a screen row,
// but the comment on `LayoutSection::leading_space` makes it clear it was originally intended for typographical
// indentation and not for screen layout
paragraph.cursor_x += leading_space;

let mut last_glyph_id = None;
Expand Down Expand Up @@ -244,34 +247,20 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec<Row>, e
let mut first_row_indentation = paragraph.glyphs[0].pos.x;
let mut row_start_x = 0.0;
let mut row_start_idx = 0;
let mut non_empty_rows = 0;

for i in 0..paragraph.glyphs.len() {
if job.wrap.max_rows <= out_rows.len() {
*elided = true;
let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x - first_row_indentation;

if job.wrap.max_rows > 0 && non_empty_rows >= job.wrap.max_rows {
break;
}

let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x;

if job.wrap.max_width < potential_row_width {
// Row break:

if first_row_indentation > 0.0
&& !row_break_candidates.has_good_candidate(job.wrap.break_anywhere)
{
// Allow the first row to be completely empty, because we know there will be more space on the next row:
// TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height.
out_rows.push(Row {
section_index_at_start: paragraph.section_index_at_start,
glyphs: vec![],
visuals: Default::default(),
rect: rect_from_x_range(first_row_indentation..=first_row_indentation),
ends_with_newline: false,
});
row_start_x += first_row_indentation;
first_row_indentation = 0.0;
} else if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere)
{
// (bu5hm4nn): we want to actually allow as much text as possible on the first line so
// we don't need a special case for the first row, but we need to subtract
// the first_row_indentation from the allowed max width
if potential_row_width > (job.wrap.max_width - first_row_indentation) {
if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere) {
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..=last_kept_index]
.iter()
.copied()
Expand All @@ -297,6 +286,12 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec<Row>, e
row_start_idx = last_kept_index + 1;
row_start_x = paragraph.glyphs[row_start_idx].pos.x;
row_break_candidates = Default::default();
non_empty_rows += 1;

// (bu5hm4nn) first row indentation gets consumed the first time it's used
if first_row_indentation > 0.0 {
first_row_indentation = 0.0
}
} else {
// Found no place to break, so we have to overrun wrap_width.
}
Expand Down Expand Up @@ -925,6 +920,7 @@ impl RowBreakCandidates {
.flatten()
}

#[allow(dead_code)]
fn has_good_candidate(&self, break_anywhere: bool) -> bool {
if break_anywhere {
self.any.is_some()
Expand Down Expand Up @@ -1062,3 +1058,36 @@ mod tests {
);
}
}

#[test]
fn test_line_break_first_row_not_empty() {
let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default());
let mut layout_job = LayoutJob::single_section(
"SomeSuperLongTextThatDoesNotHaveAnyGoodBreakCandidatesButStillNeedsToBeBroken".into(),
super::TextFormat::default(),
);

// a small area
layout_job.wrap.max_width = 110.0;

// give the first row a leading space, simulating that there already is
// text in this visual row
layout_job.sections.first_mut().unwrap().leading_space = 50.0;

let galley = super::layout(&mut fonts, layout_job.into());
assert_eq!(
galley
.rows
.iter()
.map(|row| row.glyphs.iter().map(|g| g.chr).collect::<String>())
.collect::<Vec<_>>(),
vec![
"SomeSup",
"erLongTextThat",
"DoesNotHaveAn",
"yGoodBreakCand",
"idatesButStillNe",
"edsToBeBroken"
]
);
}
Loading