mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 06:10:06 -04:00
Merge branch 'emilk:main' into main
This commit is contained in:
@@ -239,7 +239,7 @@ impl ComboBox {
|
|||||||
let mut ir = combo_box_dyn(
|
let mut ir = combo_box_dyn(
|
||||||
ui,
|
ui,
|
||||||
button_id,
|
button_id,
|
||||||
selected_text,
|
selected_text.clone(),
|
||||||
menu_contents,
|
menu_contents,
|
||||||
icon,
|
icon,
|
||||||
wrap_mode,
|
wrap_mode,
|
||||||
@@ -247,14 +247,16 @@ impl ComboBox {
|
|||||||
popup_style,
|
popup_style,
|
||||||
(width, height),
|
(width, height),
|
||||||
);
|
);
|
||||||
|
ir.response.widget_info(|| {
|
||||||
|
let mut info = WidgetInfo::new(WidgetType::ComboBox);
|
||||||
|
info.enabled = ui.is_enabled();
|
||||||
|
info.current_text_value = Some(selected_text.text().to_owned());
|
||||||
|
info
|
||||||
|
});
|
||||||
if let Some(label) = label {
|
if let Some(label) = label {
|
||||||
ir.response.widget_info(|| {
|
let label_response = ui.label(label);
|
||||||
WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text())
|
ir.response = ir.response.labelled_by(label_response.id);
|
||||||
});
|
ir.response |= label_response;
|
||||||
ir.response |= ui.label(label);
|
|
||||||
} else {
|
|
||||||
ir.response
|
|
||||||
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), ""));
|
|
||||||
}
|
}
|
||||||
ir
|
ir
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind,
|
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
|
||||||
UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
|
UiKind, UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -659,6 +659,9 @@ struct Prepared {
|
|||||||
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
|
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
|
||||||
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
|
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
|
||||||
|
|
||||||
|
/// The response from dragging the background (if enabled)
|
||||||
|
background_drag_response: Option<Response>,
|
||||||
|
|
||||||
animated: bool,
|
animated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,70 +775,72 @@ impl ScrollArea {
|
|||||||
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||||
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
|
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
|
||||||
|
|
||||||
if scroll_source.drag
|
let background_drag_response =
|
||||||
&& ui.is_enabled()
|
if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
|
||||||
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
|
// Drag contents to scroll (for touch screens mostly).
|
||||||
{
|
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||||
// Drag contents to scroll (for touch screens mostly).
|
// or we will steal input from the widgets we contain.
|
||||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
let content_response_option = state
|
||||||
// or we will steal input from the widgets we contain.
|
.interact_rect
|
||||||
let content_response_option = state
|
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
|
||||||
.interact_rect
|
|
||||||
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
|
|
||||||
|
|
||||||
if content_response_option
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|response| response.dragged())
|
|
||||||
{
|
|
||||||
for d in 0..2 {
|
|
||||||
if direction_enabled[d] {
|
|
||||||
ui.input(|input| {
|
|
||||||
state.offset[d] -= input.pointer.delta()[d];
|
|
||||||
});
|
|
||||||
state.scroll_stuck_to_end[d] = false;
|
|
||||||
state.offset_target[d] = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Apply the cursor velocity to the scroll area when the user releases the drag.
|
|
||||||
if content_response_option
|
if content_response_option
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|response| response.drag_stopped())
|
.is_some_and(|response| response.dragged())
|
||||||
{
|
{
|
||||||
state.vel =
|
for d in 0..2 {
|
||||||
direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
|
if direction_enabled[d] {
|
||||||
}
|
ui.input(|input| {
|
||||||
for d in 0..2 {
|
state.offset[d] -= input.pointer.delta()[d];
|
||||||
// Kinetic scrolling
|
});
|
||||||
let stop_speed = 20.0; // Pixels per second.
|
state.scroll_stuck_to_end[d] = false;
|
||||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
state.offset_target[d] = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Apply the cursor velocity to the scroll area when the user releases the drag.
|
||||||
|
if content_response_option
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|response| response.drag_stopped())
|
||||||
|
{
|
||||||
|
state.vel = direction_enabled.to_vec2()
|
||||||
|
* ui.input(|input| input.pointer.velocity());
|
||||||
|
}
|
||||||
|
for d in 0..2 {
|
||||||
|
// Kinetic scrolling
|
||||||
|
let stop_speed = 20.0; // Pixels per second.
|
||||||
|
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||||
|
|
||||||
let friction = friction_coeff * dt;
|
let friction = friction_coeff * dt;
|
||||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
||||||
state.vel[d] = 0.0;
|
state.vel[d] = 0.0;
|
||||||
} else {
|
} else {
|
||||||
state.vel[d] -= friction * state.vel[d].signum();
|
state.vel[d] -= friction * state.vel[d].signum();
|
||||||
// Offset has an inverted coordinate system compared to
|
// Offset has an inverted coordinate system compared to
|
||||||
// the velocity, so we subtract it instead of adding it
|
// the velocity, so we subtract it instead of adding it
|
||||||
state.offset[d] -= state.vel[d] * dt;
|
state.offset[d] -= state.vel[d] * dt;
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Set the desired mouse cursors.
|
// Set the desired mouse cursors.
|
||||||
if let Some(response) = content_response_option {
|
if let Some(response) = &content_response_option {
|
||||||
if response.dragged() {
|
if response.dragged()
|
||||||
if let Some(cursor) = on_drag_cursor {
|
&& let Some(cursor) = on_drag_cursor
|
||||||
response.on_hover_cursor(cursor);
|
{
|
||||||
|
ui.ctx().set_cursor_icon(cursor);
|
||||||
|
} else if response.hovered()
|
||||||
|
&& let Some(cursor) = on_hover_cursor
|
||||||
|
{
|
||||||
|
ui.ctx().set_cursor_icon(cursor);
|
||||||
}
|
}
|
||||||
} else if response.hovered()
|
|
||||||
&& let Some(cursor) = on_hover_cursor
|
|
||||||
{
|
|
||||||
response.on_hover_cursor(cursor);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
content_response_option
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
|
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
|
||||||
// above).
|
// above).
|
||||||
@@ -888,6 +893,7 @@ impl ScrollArea {
|
|||||||
wheel_scroll_multiplier,
|
wheel_scroll_multiplier,
|
||||||
stick_to_end,
|
stick_to_end,
|
||||||
saved_scroll_target,
|
saved_scroll_target,
|
||||||
|
background_drag_response,
|
||||||
animated,
|
animated,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1003,6 +1009,7 @@ impl Prepared {
|
|||||||
wheel_scroll_multiplier,
|
wheel_scroll_multiplier,
|
||||||
stick_to_end,
|
stick_to_end,
|
||||||
saved_scroll_target,
|
saved_scroll_target,
|
||||||
|
background_drag_response,
|
||||||
animated,
|
animated,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
@@ -1118,7 +1125,16 @@ impl Prepared {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let max_offset = content_size - inner_rect.size();
|
let max_offset = content_size - inner_rect.size();
|
||||||
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
|
|
||||||
|
// Drag-to-scroll?
|
||||||
|
let is_dragging_background = background_drag_response
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|r| r.dragged());
|
||||||
|
|
||||||
|
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
|
||||||
|
&& ui.ctx().dragged_id().is_none()
|
||||||
|
|| is_dragging_background;
|
||||||
|
|
||||||
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
|
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
|
||||||
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
|
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
|
||||||
&& direction_enabled[0] != direction_enabled[1];
|
&& direction_enabled[0] != direction_enabled[1];
|
||||||
@@ -1204,6 +1220,7 @@ impl Prepared {
|
|||||||
|
|
||||||
let is_hovering_bar_area = is_hovering_outer_rect
|
let is_hovering_bar_area = is_hovering_outer_rect
|
||||||
&& ui.rect_contains_pointer(max_bar_rect)
|
&& ui.rect_contains_pointer(max_bar_rect)
|
||||||
|
&& !is_dragging_background
|
||||||
|| state.scroll_bar_interaction[d];
|
|| state.scroll_bar_interaction[d];
|
||||||
|
|
||||||
let is_hovering_bar_area_t = ui
|
let is_hovering_bar_area_t = ui
|
||||||
|
|||||||
@@ -1129,7 +1129,10 @@ impl Visuals {
|
|||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub struct Selection {
|
pub struct Selection {
|
||||||
|
/// Background color behind selected text and other selectable buttons.
|
||||||
pub bg_fill: Color32,
|
pub bg_fill: Color32,
|
||||||
|
|
||||||
|
/// Color of selected text.
|
||||||
pub stroke: Stroke,
|
pub stroke: Stroke,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ pub fn paint_text_selection(
|
|||||||
// and so we need to clone it if it is shared:
|
// and so we need to clone it if it is shared:
|
||||||
let galley: &mut Galley = Arc::make_mut(galley);
|
let galley: &mut Galley = Arc::make_mut(galley);
|
||||||
|
|
||||||
let color = visuals.selection.bg_fill;
|
let background_color = visuals.selection.bg_fill;
|
||||||
|
let text_color = visuals.selection.stroke.color;
|
||||||
|
|
||||||
let [min, max] = cursor_range.sorted_cursors();
|
let [min, max] = cursor_range.sorted_cursors();
|
||||||
let min = galley.layout_from_cursor(min);
|
let min = galley.layout_from_cursor(min);
|
||||||
let max = galley.layout_from_cursor(max);
|
let max = galley.layout_from_cursor(max);
|
||||||
@@ -53,6 +55,31 @@ pub fn paint_text_selection(
|
|||||||
let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y));
|
let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y));
|
||||||
let mesh = &mut row.visuals.mesh;
|
let mesh = &mut row.visuals.mesh;
|
||||||
|
|
||||||
|
if !row.glyphs.is_empty() {
|
||||||
|
// Change color of the selected text:
|
||||||
|
let first_glyph_index = if ri == min.row { min.column } else { 0 };
|
||||||
|
let last_glyph_index = if ri == max.row {
|
||||||
|
max.column
|
||||||
|
} else {
|
||||||
|
row.glyphs.len() - 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let first_vertex_index = row
|
||||||
|
.glyphs
|
||||||
|
.get(first_glyph_index)
|
||||||
|
.map_or(row.visuals.glyph_vertex_range.start, |g| {
|
||||||
|
g.first_vertex as _
|
||||||
|
});
|
||||||
|
let last_vertex_index = row
|
||||||
|
.glyphs
|
||||||
|
.get(last_glyph_index)
|
||||||
|
.map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _);
|
||||||
|
|
||||||
|
for vi in first_vertex_index..last_vertex_index {
|
||||||
|
mesh.vertices[vi].color = text_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Time to insert the selection rectangle into the row mesh.
|
// Time to insert the selection rectangle into the row mesh.
|
||||||
// It should be on top (after) of any background in the galley,
|
// It should be on top (after) of any background in the galley,
|
||||||
// but behind (before) any glyphs. The row visuals has this information:
|
// but behind (before) any glyphs. The row visuals has this information:
|
||||||
@@ -60,7 +87,7 @@ pub fn paint_text_selection(
|
|||||||
|
|
||||||
// Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices):
|
// Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices):
|
||||||
let num_indices_before = mesh.indices.len();
|
let num_indices_before = mesh.indices.len();
|
||||||
mesh.add_colored_rect(rect, color);
|
mesh.add_colored_rect(rect, background_color);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
num_indices_before + 6,
|
num_indices_before + 6,
|
||||||
mesh.indices.len(),
|
mesh.indices.len(),
|
||||||
|
|||||||
@@ -223,22 +223,29 @@ impl<'a> Button<'a> {
|
|||||||
///
|
///
|
||||||
/// See also [`Self::right_text`].
|
/// See also [`Self::right_text`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
|
pub fn shortcut_text(mut self, shortcut_text: impl IntoAtoms<'a>) -> Self {
|
||||||
let mut atom = shortcut_text.into();
|
|
||||||
atom.kind = match atom.kind {
|
|
||||||
AtomKind::Text(text) => AtomKind::Text(text.weak()),
|
|
||||||
other => other,
|
|
||||||
};
|
|
||||||
self.layout.push_right(Atom::grow());
|
self.layout.push_right(Atom::grow());
|
||||||
self.layout.push_right(atom);
|
|
||||||
|
for mut atom in shortcut_text.into_atoms() {
|
||||||
|
atom.kind = match atom.kind {
|
||||||
|
AtomKind::Text(text) => AtomKind::Text(text.weak()),
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
self.layout.push_right(atom);
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show some text on the right side of the button.
|
/// Show some text on the right side of the button.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
|
pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self {
|
||||||
self.layout.push_right(Atom::grow());
|
self.layout.push_right(Atom::grow());
|
||||||
self.layout.push_right(right_text.into());
|
|
||||||
|
for atom in right_text.into_atoms() {
|
||||||
|
self.layout.push_right(atom);
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use egui_kittest::Harness;
|
use egui::{Color32, accesskit::Role};
|
||||||
|
use egui_kittest::{Harness, kittest::Queryable as _};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_kerning() {
|
fn test_kerning() {
|
||||||
@@ -42,7 +43,7 @@ fn test_italics() {
|
|||||||
harness.run();
|
harness.run();
|
||||||
harness.fit_contents();
|
harness.fit_contents();
|
||||||
harness.snapshot(format!(
|
harness.snapshot(format!(
|
||||||
"image_blending/image_{theme}_x{pixels_per_point:.2}",
|
"italics/image_{theme}_x{pixels_per_point:.2}",
|
||||||
theme = match theme {
|
theme = match theme {
|
||||||
egui::Theme::Dark => "dark",
|
egui::Theme::Dark => "dark",
|
||||||
egui::Theme::Light => "light",
|
egui::Theme::Light => "light",
|
||||||
@@ -51,3 +52,24 @@ fn test_italics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_text_selection() {
|
||||||
|
let mut harness = Harness::builder().build_ui(|ui| {
|
||||||
|
let visuals = ui.visuals_mut();
|
||||||
|
visuals.selection.bg_fill = Color32::LIGHT_GREEN;
|
||||||
|
visuals.selection.stroke.color = Color32::DARK_BLUE;
|
||||||
|
|
||||||
|
ui.label("Some varied ☺ text :)\nAnd it has a second line!");
|
||||||
|
});
|
||||||
|
harness.run();
|
||||||
|
harness.fit_contents();
|
||||||
|
|
||||||
|
// Drag to select text:
|
||||||
|
let label = harness.get_by_role(Role::Label);
|
||||||
|
harness.drag_at(label.rect().lerp_inside([0.2, 0.25]));
|
||||||
|
harness.drop_at(label.rect().lerp_inside([0.6, 0.75]));
|
||||||
|
harness.run();
|
||||||
|
|
||||||
|
harness.snapshot("text_selection");
|
||||||
|
}
|
||||||
|
|||||||
3
crates/egui_demo_lib/tests/snapshots/text_selection.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/text_selection.png
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:14f253fedc94985ff1431f1016d901d747e1f9948531cc6350f6615649f29056
|
||||||
|
size 4862
|
||||||
@@ -8,9 +8,7 @@ mod builder;
|
|||||||
mod snapshot;
|
mod snapshot;
|
||||||
|
|
||||||
#[cfg(feature = "snapshot")]
|
#[cfg(feature = "snapshot")]
|
||||||
pub use snapshot::*;
|
pub use crate::snapshot::*;
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
mod app_kind;
|
mod app_kind;
|
||||||
mod node;
|
mod node;
|
||||||
@@ -20,19 +18,26 @@ mod texture_to_image;
|
|||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
pub mod wgpu;
|
pub mod wgpu;
|
||||||
|
|
||||||
pub use kittest;
|
// re-exports:
|
||||||
|
pub use {
|
||||||
|
self::{builder::*, node::*, renderer::*},
|
||||||
|
kittest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display, Formatter},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use egui::{
|
||||||
|
Color32, Key, Modifiers, PointerButton, Pos2, Rect, RepaintCause, Shape, Vec2, ViewportId,
|
||||||
|
epaint::{ClippedShape, RectShape},
|
||||||
|
style::ScrollAnimation,
|
||||||
|
};
|
||||||
|
use kittest::Queryable;
|
||||||
|
|
||||||
use crate::app_kind::AppKind;
|
use crate::app_kind::AppKind;
|
||||||
|
|
||||||
pub use builder::*;
|
|
||||||
pub use node::*;
|
|
||||||
pub use renderer::*;
|
|
||||||
|
|
||||||
use egui::epaint::{ClippedShape, RectShape};
|
|
||||||
use egui::style::ScrollAnimation;
|
|
||||||
use egui::{Color32, Key, Modifiers, Pos2, Rect, RepaintCause, Shape, Vec2, ViewportId};
|
|
||||||
use kittest::Queryable;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ExceededMaxStepsError {
|
pub struct ExceededMaxStepsError {
|
||||||
pub max_steps: u64,
|
pub max_steps: u64,
|
||||||
@@ -598,6 +603,32 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
self.key_combination_modifiers(modifiers, &[key]);
|
self.key_combination_modifiers(modifiers, &[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move mouse cursor to this position.
|
||||||
|
pub fn hover_at(&self, pos: egui::Pos2) {
|
||||||
|
self.event(egui::Event::PointerMoved(pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start dragging from a position.
|
||||||
|
pub fn drag_at(&self, pos: egui::Pos2) {
|
||||||
|
self.event(egui::Event::PointerButton {
|
||||||
|
pos,
|
||||||
|
button: PointerButton::Primary,
|
||||||
|
pressed: true,
|
||||||
|
modifiers: Modifiers::NONE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop dragging and remove cursor.
|
||||||
|
pub fn drop_at(&self, pos: egui::Pos2) {
|
||||||
|
self.event(egui::Event::PointerButton {
|
||||||
|
pos,
|
||||||
|
button: PointerButton::Primary,
|
||||||
|
pressed: false,
|
||||||
|
modifiers: Modifiers::NONE,
|
||||||
|
});
|
||||||
|
self.remove_cursor();
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove the cursor from the screen.
|
/// Remove the cursor from the screen.
|
||||||
///
|
///
|
||||||
/// Will fire a [`egui::Event::PointerGone`] event.
|
/// Will fire a [`egui::Event::PointerGone`] event.
|
||||||
|
|||||||
@@ -449,7 +449,8 @@ impl Rect {
|
|||||||
/// Linearly interpolate so that `[0, 0]` is [`Self::min`] and
|
/// Linearly interpolate so that `[0, 0]` is [`Self::min`] and
|
||||||
/// `[1, 1]` is [`Self::max`].
|
/// `[1, 1]` is [`Self::max`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn lerp_inside(&self, t: Vec2) -> Pos2 {
|
pub fn lerp_inside(&self, t: impl Into<Vec2>) -> Pos2 {
|
||||||
|
let t = t.into();
|
||||||
Pos2 {
|
Pos2 {
|
||||||
x: lerp(self.min.x..=self.max.x, t.x),
|
x: lerp(self.min.x..=self.max.x, t.x),
|
||||||
y: lerp(self.min.y..=self.max.y, t.y),
|
y: lerp(self.min.y..=self.max.y, t.y),
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ fn layout_section(
|
|||||||
font_ascent: font_metrics.ascent,
|
font_ascent: font_metrics.ascent,
|
||||||
uv_rect: glyph_alloc.uv_rect,
|
uv_rect: glyph_alloc.uv_rect,
|
||||||
section_index,
|
section_index,
|
||||||
|
first_vertex: 0, // filled in later
|
||||||
});
|
});
|
||||||
|
|
||||||
paragraph.cursor_x_px += glyph_alloc.advance_width_px;
|
paragraph.cursor_x_px += glyph_alloc.advance_width_px;
|
||||||
@@ -531,6 +532,7 @@ fn replace_last_glyph_with_overflow_character(
|
|||||||
font_ascent: font_metrics.ascent,
|
font_ascent: font_metrics.ascent,
|
||||||
uv_rect: replacement_glyph_alloc.uv_rect,
|
uv_rect: replacement_glyph_alloc.uv_rect,
|
||||||
section_index,
|
section_index,
|
||||||
|
first_vertex: 0, // filled in later
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -748,7 +750,7 @@ fn tessellate_row(
|
|||||||
point_scale: PointScale,
|
point_scale: PointScale,
|
||||||
job: &LayoutJob,
|
job: &LayoutJob,
|
||||||
format_summary: &FormatSummary,
|
format_summary: &FormatSummary,
|
||||||
row: &Row,
|
row: &mut Row,
|
||||||
) -> RowVisuals {
|
) -> RowVisuals {
|
||||||
if row.glyphs.is_empty() {
|
if row.glyphs.is_empty() {
|
||||||
return Default::default();
|
return Default::default();
|
||||||
@@ -843,8 +845,9 @@ fn add_row_backgrounds(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh
|
|||||||
end_run(run_start.take(), last_rect.right());
|
end_run(run_start.take(), last_rect.right());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tessellate_glyphs(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh: &mut Mesh) {
|
fn tessellate_glyphs(point_scale: PointScale, job: &LayoutJob, row: &mut Row, mesh: &mut Mesh) {
|
||||||
for glyph in &row.glyphs {
|
for glyph in &mut row.glyphs {
|
||||||
|
glyph.first_vertex = mesh.vertices.len() as u32;
|
||||||
let uv_rect = glyph.uv_rect;
|
let uv_rect = glyph.uv_rect;
|
||||||
if !uv_rect.is_nothing() {
|
if !uv_rect.is_nothing() {
|
||||||
let mut left_top = glyph.pos + uv_rect.offset;
|
let mut left_top = glyph.pos + uv_rect.offset;
|
||||||
|
|||||||
@@ -701,6 +701,9 @@ pub struct Glyph {
|
|||||||
/// enable the paragraph-concat optimization path without having to
|
/// enable the paragraph-concat optimization path without having to
|
||||||
/// adjust `section_index` when concatting.
|
/// adjust `section_index` when concatting.
|
||||||
pub(crate) section_index: u32,
|
pub(crate) section_index: u32,
|
||||||
|
|
||||||
|
/// Which is our first vertex in [`RowVisuals::mesh`].
|
||||||
|
pub first_vertex: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Glyph {
|
impl Glyph {
|
||||||
|
|||||||
@@ -61,3 +61,17 @@ fn text_edit_rtl() {
|
|||||||
harness.snapshot(format!("text_edit_rtl_{i}"));
|
harness.snapshot(format!("text_edit_rtl_{i}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combobox_should_have_value() {
|
||||||
|
let harness = Harness::new_ui(|ui| {
|
||||||
|
egui::ComboBox::from_label("Select an option")
|
||||||
|
.selected_text("Option 1")
|
||||||
|
.show_ui(ui, |_ui| {});
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
harness.get_by_label("Select an option").value().as_deref(),
|
||||||
|
Some("Option 1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
3
tests/egui_tests/tests/snapshots/button_shortcut.png
Normal file
3
tests/egui_tests/tests/snapshots/button_shortcut.png
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5befd84158b582c79a968f36e43c7017187b364824eb4470b048d133e62f9360
|
||||||
|
size 1600
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0f107d95fee9a5fb5fbfd2422452e1820738a84c81774587dbfa8153e91e4c73
|
||||||
|
size 414552
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6c1aebada9349f8cb4046469b0a6f9796a21f88b6724bd85cd832a40b8007409
|
||||||
|
size 540527
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:685de2e33ff26aafa87426bcda18bb9963c2deb2a811cd0aae4450af0e245a06
|
||||||
|
size 390735
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cf4236b1a8f63d184cd780c334d9f996e4d47817a96a29f0d81658d2d897597f
|
||||||
|
size 10529
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c7a63953853f526b83f80d63335b03e60258ea9a3416d19f8ed57d746b5c551d
|
||||||
|
size 21557
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1f7d802a4de7e30f8d254cab6d9ca127866c104c1738103bc4a579917e8f42d3
|
||||||
|
size 9850
|
||||||
@@ -108,3 +108,14 @@ fn test_intrinsic_size() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_shortcut_text() {
|
||||||
|
let mut harness = HarnessBuilder::default().build_ui(|ui| {
|
||||||
|
ui.add(egui::Button::new("Click me").shortcut_text(("1", "2", "3")));
|
||||||
|
});
|
||||||
|
harness.run();
|
||||||
|
harness.fit_contents();
|
||||||
|
|
||||||
|
harness.snapshot("button_shortcut");
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use egui::accesskit::Role;
|
|||||||
use egui::load::SizedTexture;
|
use egui::load::SizedTexture;
|
||||||
use egui::{
|
use egui::{
|
||||||
Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, DragValue, Event,
|
Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, DragValue, Event,
|
||||||
Grid, IntoAtoms as _, Layout, PointerButton, Response, Slider, Stroke, StrokeKind,
|
Grid, IntoAtoms as _, Layout, PointerButton, Response, Slider, Stroke, StrokeKind, TextEdit,
|
||||||
TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, include_image,
|
TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, include_image,
|
||||||
};
|
};
|
||||||
use egui_kittest::kittest::{Queryable as _, by};
|
use egui_kittest::kittest::{Queryable as _, by};
|
||||||
@@ -84,6 +84,37 @@ fn widget_tests() {
|
|||||||
},
|
},
|
||||||
&mut results,
|
&mut results,
|
||||||
);
|
);
|
||||||
|
test_widget(
|
||||||
|
"text_edit_clip",
|
||||||
|
|ui| {
|
||||||
|
ui.spacing_mut().text_edit_width = 45.0;
|
||||||
|
TextEdit::singleline(&mut "This is a very very long text".to_owned())
|
||||||
|
.clip_text(true)
|
||||||
|
.ui(ui)
|
||||||
|
},
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
test_widget(
|
||||||
|
"text_edit_no_clip",
|
||||||
|
|ui| {
|
||||||
|
ui.spacing_mut().text_edit_width = 45.0;
|
||||||
|
TextEdit::singleline(&mut "This is a very very long text".to_owned())
|
||||||
|
.clip_text(false)
|
||||||
|
.ui(ui)
|
||||||
|
},
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
test_widget(
|
||||||
|
"text_edit_placeholder_clip",
|
||||||
|
|ui| {
|
||||||
|
ui.spacing_mut().text_edit_width = 45.0;
|
||||||
|
TextEdit::singleline(&mut String::new())
|
||||||
|
.hint_text("This is a very very long placeholder")
|
||||||
|
.clip_text(true)
|
||||||
|
.ui(ui)
|
||||||
|
},
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
|
||||||
test_widget(
|
test_widget(
|
||||||
"slider",
|
"slider",
|
||||||
|
|||||||
Reference in New Issue
Block a user