mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 21:00:03 -04:00
Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports
This commit is contained in:
20
README.md
20
README.md
@@ -87,7 +87,7 @@ On Fedora Rawhide you need to run:
|
||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
|
||||
* Modular: You should be able to use small parts of egui and combine them in new ways
|
||||
* Safe: there is no `unsafe` code in egui
|
||||
* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`nohash-hasher`](https://crates.io/crates/nohash-hasher) [`parking_lot`](https://crates.io/crates/parking_lot)
|
||||
* Minimal dependencies
|
||||
|
||||
egui is *not* a framework. egui is a library you call into, not an environment you program for.
|
||||
|
||||
@@ -99,6 +99,21 @@ egui is *not* a framework. egui is a library you call into, not an environment y
|
||||
* Native looking interface
|
||||
* Advanced and flexible layouts (that's fundamentally incompatible with immediate mode)
|
||||
|
||||
## Dependencies
|
||||
`egui` has a minimal set of default dependencies:
|
||||
|
||||
* [`ab_glyph`](https://crates.io/crates/ab_glyph)
|
||||
* [`ahash`](https://crates.io/crates/ahash)
|
||||
* [`nohash-hasher`](https://crates.io/crates/nohash-hasher)
|
||||
* [`parking_lot`](https://crates.io/crates/parking_lot)
|
||||
|
||||
Heavier dependencies are kept out of `egui`, even as opt-in.
|
||||
No code that isn't fully Wasm-friendly is part of `egui`.
|
||||
|
||||
To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/master/crates/egui_extras) crate.
|
||||
|
||||
[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc,
|
||||
|
||||
## Who is egui for?
|
||||
|
||||
egui aims to be the best choice when you want a simple way to create a GUI, or you want to add a GUI to a game engine.
|
||||
@@ -351,6 +366,9 @@ Notable contributions by:
|
||||
* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868).
|
||||
* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050).
|
||||
* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625).
|
||||
* [@mwcampbell](https://github.com/mwcampbell): [AccessKit](https://github.com/AccessKit/accesskit) [integration](https://github.com/emilk/egui/pull/2294).
|
||||
* [@hasenbanck](https://github.com/hasenbanck), [@s-nie](https://github.com/s-nie), [@Wumpf](https://github.com/Wumpf): [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)
|
||||
* [@jprochazk](https://github.com/jprochazk): [egui image API](https://github.com/emilk/egui/issues/3291)
|
||||
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
|
||||
|
||||
egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).
|
||||
|
||||
@@ -155,6 +155,12 @@ impl Color32 {
|
||||
Self([r, g, b, 0])
|
||||
}
|
||||
|
||||
/// Is the alpha=0 ?
|
||||
#[inline(always)]
|
||||
pub fn is_additive(self) -> bool {
|
||||
self.a() == 0
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub const fn to_array(&self) -> [u8; 4] {
|
||||
|
||||
@@ -122,6 +122,12 @@ impl Rgba {
|
||||
Self([r, g, b, 0.0])
|
||||
}
|
||||
|
||||
/// Is the alpha=0 ?
|
||||
#[inline(always)]
|
||||
pub fn is_additive(self) -> bool {
|
||||
self.a() == 0.0
|
||||
}
|
||||
|
||||
/// Multiply with e.g. 0.5 to make us half transparent
|
||||
#[inline(always)]
|
||||
pub fn multiply(self, alpha: f32) -> Self {
|
||||
|
||||
@@ -1560,7 +1560,7 @@ mod glow_integration {
|
||||
if win.read().window.as_ref().unwrap().read().is_minimized() == Some(true) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
crate::profile_scope!("bg_sleep");
|
||||
crate::profile_scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
@@ -2479,7 +2479,7 @@ mod wgpu_integration {
|
||||
if window.read().is_minimized() == Some(true) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
crate::profile_scope!("bg_sleep");
|
||||
crate::profile_scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ impl State {
|
||||
.events
|
||||
.push(egui::Event::CompositionEnd(text.clone()));
|
||||
}
|
||||
winit::event::Ime::Preedit(text, ..) => {
|
||||
winit::event::Ime::Preedit(text, Some(_)) => {
|
||||
if !self.input_method_editor_started {
|
||||
self.input_method_editor_started = true;
|
||||
self.egui_input.events.push(egui::Event::CompositionStart);
|
||||
@@ -349,6 +349,7 @@ impl State {
|
||||
.events
|
||||
.push(egui::Event::CompositionUpdate(text.clone()));
|
||||
}
|
||||
winit::event::Ime::Preedit(_, None) => {}
|
||||
};
|
||||
|
||||
EventResponse {
|
||||
|
||||
@@ -734,10 +734,6 @@ impl Context {
|
||||
) -> R {
|
||||
self.write(move |ctx| writer(&mut ctx.memory.options.tessellation_options))
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// If the given [`Id`] has been used previously the same frame at at different position,
|
||||
/// then an error will be printed on screen.
|
||||
@@ -1104,6 +1100,31 @@ impl Context {
|
||||
self.output_mut(|o| o.cursor_icon = cursor_icon);
|
||||
}
|
||||
|
||||
/// Open an URL in a browser.
|
||||
///
|
||||
/// Equivalent to:
|
||||
/// ```
|
||||
/// # let ctx = egui::Context::default();
|
||||
/// # let open_url = egui::OpenUrl::same_tab("http://www.example.com");
|
||||
/// ctx.output_mut(|o| o.open_url = Some(open_url));
|
||||
/// ```
|
||||
pub fn open_url(&self, open_url: crate::OpenUrl) {
|
||||
self.output_mut(|o| o.open_url = Some(open_url));
|
||||
}
|
||||
|
||||
/// Copy the given text to the system clipboard.
|
||||
///
|
||||
/// Empty strings are ignored.
|
||||
///
|
||||
/// Equivalent to:
|
||||
/// ```
|
||||
/// # let ctx = egui::Context::default();
|
||||
/// ctx.output_mut(|o| o.copied_text = "Copy this".to_owned());
|
||||
/// ```
|
||||
pub fn copy_text(&self, text: String) {
|
||||
self.output_mut(|o| o.copied_text = text);
|
||||
}
|
||||
|
||||
/// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`).
|
||||
///
|
||||
/// Can be used to get the text for [`Button::shortcut_text`].
|
||||
|
||||
@@ -93,6 +93,8 @@ pub struct PlatformOutput {
|
||||
pub mutable_text_under_cursor: bool,
|
||||
|
||||
/// Screen-space position of text edit cursor (used for IME).
|
||||
///
|
||||
/// Iff `Some`, the user is editing text.
|
||||
pub text_cursor_pos: Option<crate::Pos2>,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
@@ -101,7 +103,9 @@ pub struct PlatformOutput {
|
||||
|
||||
impl PlatformOutput {
|
||||
/// Open the given url in a web browser.
|
||||
///
|
||||
/// If egui is running in a browser, the same tab will be reused.
|
||||
#[deprecated = "Use Context::open_url instead"]
|
||||
pub fn open_url(&mut self, url: impl ToString) {
|
||||
self.open_url = Some(OpenUrl::same_tab(url));
|
||||
}
|
||||
@@ -165,6 +169,8 @@ impl PlatformOutput {
|
||||
}
|
||||
|
||||
/// What URL to open, and how.
|
||||
///
|
||||
/// Use with [`crate::Context::open_url`].
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct OpenUrl {
|
||||
|
||||
@@ -390,7 +390,9 @@ pub use {
|
||||
context::{Context, RequestRepaintInfo},
|
||||
data::{
|
||||
input::*,
|
||||
output::{self, CursorIcon, FullOutput, PlatformOutput, UserAttentionType, WidgetInfo},
|
||||
output::{
|
||||
self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo,
|
||||
},
|
||||
},
|
||||
grid::Grid,
|
||||
id::{Id, IdMap},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs
|
||||
|
||||
use crate::{
|
||||
area, window, EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rect, Style, ViewportId,
|
||||
};
|
||||
@@ -19,6 +21,7 @@ use epaint::{emath::Rangef, vec2, Vec2};
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "persistence", serde(default))]
|
||||
pub struct Memory {
|
||||
/// Global egui options.
|
||||
pub options: Options,
|
||||
|
||||
/// This map stores some superficial state for all widgets with custom [`Id`]s.
|
||||
@@ -692,21 +695,25 @@ impl Memory {
|
||||
self.interaction.focus.focused_widget = None;
|
||||
}
|
||||
|
||||
/// Is any widget being dragged?
|
||||
#[inline(always)]
|
||||
pub fn is_anything_being_dragged(&self) -> bool {
|
||||
self.interaction.drag_id.is_some()
|
||||
}
|
||||
|
||||
/// Is this specific widget being dragged?
|
||||
#[inline(always)]
|
||||
pub fn is_being_dragged(&self, id: Id) -> bool {
|
||||
self.interaction.drag_id == Some(id)
|
||||
}
|
||||
|
||||
/// Set which widget is being dragged.
|
||||
#[inline(always)]
|
||||
pub fn set_dragged_id(&mut self, id: Id) {
|
||||
self.interaction.drag_id = Some(id);
|
||||
}
|
||||
|
||||
/// Stop dragging any widget.
|
||||
#[inline(always)]
|
||||
pub fn stop_dragging(&mut self) {
|
||||
self.interaction.drag_id = None;
|
||||
@@ -728,22 +735,29 @@ impl Memory {
|
||||
/// Popups are things like combo-boxes, color pickers, menus etc.
|
||||
/// Only one can be be open at a time.
|
||||
impl Memory {
|
||||
/// Is the given popup open?
|
||||
pub fn is_popup_open(&self, popup_id: Id) -> bool {
|
||||
self.popup == Some(popup_id) || self.everything_is_visible()
|
||||
}
|
||||
|
||||
/// Is any popup open?
|
||||
pub fn any_popup_open(&self) -> bool {
|
||||
self.popup.is_some() || self.everything_is_visible()
|
||||
}
|
||||
|
||||
/// Open the given popup, and close all other.
|
||||
pub fn open_popup(&mut self, popup_id: Id) {
|
||||
self.popup = Some(popup_id);
|
||||
}
|
||||
|
||||
/// Close the open popup, if any.
|
||||
pub fn close_popup(&mut self) {
|
||||
self.popup = None;
|
||||
}
|
||||
|
||||
/// Toggle the given popup between closed and open.
|
||||
///
|
||||
/// Note: at most one popup can be open at one time.
|
||||
pub fn toggle_popup(&mut self, popup_id: Id) {
|
||||
if self.is_popup_open(popup_id) {
|
||||
self.close_popup();
|
||||
|
||||
@@ -234,9 +234,9 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>, alpha: Alpha) {
|
||||
|
||||
if ui.button("📋").on_hover_text("Click to copy").clicked() {
|
||||
if alpha == Alpha::Opaque {
|
||||
ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}"));
|
||||
ui.ctx().copy_text(format!("{r}, {g}, {b}"));
|
||||
} else {
|
||||
ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}, {a}"));
|
||||
ui.ctx().copy_text(format!("{r}, {g}, {b}, {a}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,21 +118,18 @@ impl Widget for Hyperlink {
|
||||
let Self { url, text, new_tab } = self;
|
||||
|
||||
let response = ui.add(Link::new(text));
|
||||
|
||||
if response.clicked() {
|
||||
let modifiers = ui.ctx().input(|i| i.modifiers);
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.open_url = Some(crate::output::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: new_tab || modifiers.any(),
|
||||
});
|
||||
ui.ctx().open_url(crate::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: new_tab || modifiers.any(),
|
||||
});
|
||||
}
|
||||
if response.middle_clicked() {
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.open_url = Some(crate::output::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: true,
|
||||
});
|
||||
ui.ctx().open_url(crate::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: true,
|
||||
});
|
||||
}
|
||||
response.on_hover_text(url)
|
||||
|
||||
@@ -900,7 +900,7 @@ fn events(
|
||||
|
||||
let copy_if_not_password = |ui: &Ui, text: String| {
|
||||
if !password {
|
||||
ui.ctx().output_mut(|o| o.copied_text = text);
|
||||
ui.ctx().copy_text(text);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
|
||||
if let Some(text) = &text {
|
||||
let tooltip = "Click to copy the response body";
|
||||
if ui.button("📋").on_hover_text(tooltip).clicked() {
|
||||
ui.output_mut(|o| o.copied_text = text.clone());
|
||||
ui.ctx().copy_text(text.clone());
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
@@ -396,7 +396,8 @@ impl WrapApp {
|
||||
{
|
||||
selected_anchor = anchor;
|
||||
if frame.is_web() {
|
||||
ui.output_mut(|o| o.open_url(format!("#{anchor}")));
|
||||
ui.ctx()
|
||||
.open_url(egui::OpenUrl::same_tab(format!("#{anchor}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,7 +409,7 @@ impl WrapApp {
|
||||
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
|
||||
self.state.selected_anchor = Anchor::Clock;
|
||||
if frame.is_web() {
|
||||
ui.output_mut(|o| o.open_url("#clock"));
|
||||
ui.ctx().open_url(egui::OpenUrl::same_tab("#clock"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ impl super::View for FontBook {
|
||||
};
|
||||
|
||||
if ui.add(button).on_hover_ui(tooltip_ui).clicked() {
|
||||
ui.output_mut(|o| o.copied_text = chr.to_string());
|
||||
ui.ctx().copy_text(chr.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,18 @@ pub struct TextLayoutDemo {
|
||||
overflow_character: Option<char>,
|
||||
extra_letter_spacing_pixels: i32,
|
||||
line_height_pixels: u32,
|
||||
lorem_ipsum: bool,
|
||||
}
|
||||
|
||||
impl Default for TextLayoutDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_rows: 3,
|
||||
max_rows: 6,
|
||||
break_anywhere: true,
|
||||
overflow_character: Some('…'),
|
||||
extra_letter_spacing_pixels: 0,
|
||||
line_height_pixels: 0,
|
||||
lorem_ipsum: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +47,7 @@ impl super::View for TextLayoutDemo {
|
||||
overflow_character,
|
||||
extra_letter_spacing_pixels,
|
||||
line_height_pixels,
|
||||
lorem_ipsum,
|
||||
} = self;
|
||||
|
||||
use egui::text::LayoutJob;
|
||||
@@ -104,32 +107,55 @@ impl super::View for TextLayoutDemo {
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Text:");
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(lorem_ipsum, true, "Lorem Ipsum");
|
||||
ui.selectable_value(lorem_ipsum, false, "La Pasionaria");
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
let extra_letter_spacing = points_per_pixel * *extra_letter_spacing_pixels as f32;
|
||||
let line_height =
|
||||
(*line_height_pixels != 0).then_some(points_per_pixel * *line_height_pixels as f32);
|
||||
let text = if *lorem_ipsum {
|
||||
crate::LOREM_IPSUM_LONG
|
||||
} else {
|
||||
TO_BE_OR_NOT_TO_BE
|
||||
};
|
||||
|
||||
let mut job = LayoutJob::single_section(
|
||||
crate::LOREM_IPSUM_LONG.to_owned(),
|
||||
egui::TextFormat {
|
||||
extra_letter_spacing,
|
||||
line_height,
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
let extra_letter_spacing = points_per_pixel * *extra_letter_spacing_pixels as f32;
|
||||
let line_height = (*line_height_pixels != 0)
|
||||
.then_some(points_per_pixel * *line_height_pixels as f32);
|
||||
|
||||
let mut job = LayoutJob::single_section(
|
||||
text.to_owned(),
|
||||
egui::TextFormat {
|
||||
extra_letter_spacing,
|
||||
line_height,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.wrap = egui::text::TextWrapping {
|
||||
max_rows: *max_rows,
|
||||
break_anywhere: *break_anywhere,
|
||||
overflow_character: *overflow_character,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.wrap = egui::text::TextWrapping {
|
||||
max_rows: *max_rows,
|
||||
break_anywhere: *break_anywhere,
|
||||
overflow_character: *overflow_character,
|
||||
..Default::default()
|
||||
};
|
||||
};
|
||||
|
||||
// NOTE: `Label` overrides some of the wrapping settings, e.g. wrap width
|
||||
ui.label(job);
|
||||
});
|
||||
// NOTE: `Label` overrides some of the wrapping settings, e.g. wrap width
|
||||
ui.label(job);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Excerpt from Dolores Ibárruri's farwel speech to the International Brigades:
|
||||
const TO_BE_OR_NOT_TO_BE: &str = "Mothers! Women!\n
|
||||
When the years pass by and the wounds of war are stanched; when the memory of the sad and bloody days dissipates in a present of liberty, of peace and of wellbeing; when the rancor have died out and pride in a free country is felt equally by all Spaniards, speak to your children. Tell them of these men of the International Brigades.\n\
|
||||
\n\
|
||||
Recount for them how, coming over seas and mountains, crossing frontiers bristling with bayonets, sought by raving dogs thirsting to tear their flesh, these men reached our country as crusaders for freedom, to fight and die for Spain’s liberty and independence threatened by German and Italian fascism. \
|
||||
They gave up everything — their loves, their countries, home and fortune, fathers, mothers, wives, brothers, sisters and children — and they came and said to us: “We are here. Your cause, Spain’s cause, is ours. It is the cause of all advanced and progressive mankind.”\n\
|
||||
\n\
|
||||
- Dolores Ibárruri, 1938";
|
||||
|
||||
@@ -7,3 +7,15 @@
|
||||

|
||||
|
||||
This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`.
|
||||
|
||||
## Images
|
||||
One thing `egui_extras` is commonly used for is to install image loaders for `egui`:
|
||||
|
||||
```toml
|
||||
egui_extras = { version = "*", features = ["all_loaders"] }
|
||||
image = { version = "0.24", features = ["jpeg", "png"] }
|
||||
```
|
||||
|
||||
```rs
|
||||
egui_extras::install_image_loaders(egui_ctx);
|
||||
```
|
||||
|
||||
@@ -7,6 +7,7 @@ pub(crate) struct DatePickerButtonState {
|
||||
pub picker_visible: bool,
|
||||
}
|
||||
|
||||
/// Shows a date, and will open a date picker popup when clicked.
|
||||
pub struct DatePickerButton<'a> {
|
||||
selection: &'a mut NaiveDate,
|
||||
id_source: Option<&'a str>,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub mod syntax_highlighting;
|
||||
#[doc(hidden)]
|
||||
pub mod image;
|
||||
mod layout;
|
||||
pub mod loaders;
|
||||
mod loaders;
|
||||
mod sizing;
|
||||
mod strip;
|
||||
mod table;
|
||||
|
||||
@@ -508,6 +508,7 @@ impl Language {
|
||||
"c" | "h" | "hpp" | "cpp" | "c++" => Some(Self::cpp()),
|
||||
"py" | "python" => Some(Self::python()),
|
||||
"rs" | "rust" => Some(Self::rust()),
|
||||
"toml" => Some(Self::toml()),
|
||||
_ => {
|
||||
None // unsupported language
|
||||
}
|
||||
@@ -655,4 +656,12 @@ impl Language {
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn toml() -> Self {
|
||||
Self {
|
||||
double_slash_comments: false,
|
||||
hash_comments: true,
|
||||
keywords: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,16 @@ pub(super) trait RectElement {
|
||||
|
||||
pub(super) fn highlighted_color(mut stroke: Stroke, fill: Color32) -> (Stroke, Color32) {
|
||||
stroke.width *= 2.0;
|
||||
let fill = Rgba::from(fill);
|
||||
let fill_alpha = (2.0 * fill.a()).at_most(1.0);
|
||||
let fill = fill.to_opaque().multiply(fill_alpha);
|
||||
|
||||
let mut fill = Rgba::from(fill);
|
||||
if fill.is_additive() {
|
||||
// Make slightly brighter
|
||||
fill = 1.3 * fill;
|
||||
} else {
|
||||
// Make more opaque:
|
||||
let fill_alpha = (2.0 * fill.a()).at_most(1.0);
|
||||
fill = fill.to_opaque().multiply(fill_alpha);
|
||||
}
|
||||
|
||||
(stroke, fill.into())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ use crate::*;
|
||||
/// of `min` and `max` are swapped. These are usually a sign of an error.
|
||||
///
|
||||
/// Normally the unit is points (logical pixels) in screen space coordinates.
|
||||
///
|
||||
/// `Rect` does NOT implement `Default`, because there is no obvious default value.
|
||||
/// [`Rect::ZERO`] may seem reasonable, but when used as a bounding box, [`Rect::NOTHING`]
|
||||
/// is a better default - so be explicit instead!
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
|
||||
use emath::*;
|
||||
|
||||
use crate::{Color32, Mesh, Stroke, Vertex};
|
||||
use crate::{text::font::Font, Color32, Mesh, Stroke, Vertex};
|
||||
|
||||
use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals};
|
||||
|
||||
@@ -40,17 +40,31 @@ impl PointScale {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Temporary storage before line-wrapping.
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Clone)]
|
||||
struct Paragraph {
|
||||
/// Start of the next glyph to be added.
|
||||
pub cursor_x: f32,
|
||||
|
||||
/// This is included in case there are no glyphs
|
||||
pub section_index_at_start: u32,
|
||||
|
||||
pub glyphs: Vec<Glyph>,
|
||||
|
||||
/// In case of an empty paragraph ("\n"), use this as height.
|
||||
pub empty_paragraph_height: f32,
|
||||
}
|
||||
|
||||
impl Paragraph {
|
||||
pub fn from_section_index(section_index_at_start: u32) -> Self {
|
||||
Self {
|
||||
cursor_x: 0.0,
|
||||
section_index_at_start,
|
||||
glyphs: vec![],
|
||||
empty_paragraph_height: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout text into a [`Galley`].
|
||||
///
|
||||
/// In most cases you should use [`crate::Fonts::layout_job`] instead
|
||||
@@ -70,7 +84,9 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||
};
|
||||
}
|
||||
|
||||
let mut paragraphs = vec![Paragraph::default()];
|
||||
// For most of this we ignore the y coordinate:
|
||||
|
||||
let mut paragraphs = vec![Paragraph::from_section_index(0)];
|
||||
for (section_index, section) in job.sections.iter().enumerate() {
|
||||
layout_section(fonts, &job, section_index as u32, section, &mut paragraphs);
|
||||
}
|
||||
@@ -78,7 +94,12 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||
let point_scale = PointScale::new(fonts.pixels_per_point());
|
||||
|
||||
let mut elided = false;
|
||||
let mut rows = rows_from_paragraphs(fonts, paragraphs, &job, &mut elided);
|
||||
let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided);
|
||||
if elided {
|
||||
if let Some(last_row) = rows.last_mut() {
|
||||
replace_last_glyph_with_overflow_character(fonts, &job, last_row);
|
||||
}
|
||||
}
|
||||
|
||||
let justify = job.justify && job.wrap.max_width.is_finite();
|
||||
|
||||
@@ -97,9 +118,11 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the Y positions and tessellate the text:
|
||||
galley_from_rows(point_scale, job, rows, elided)
|
||||
}
|
||||
|
||||
// Ignores the Y coordinate.
|
||||
fn layout_section(
|
||||
fonts: &mut FontsImpl,
|
||||
job: &LayoutJob,
|
||||
@@ -130,7 +153,7 @@ fn layout_section(
|
||||
|
||||
for chr in job.text[byte_range.clone()].chars() {
|
||||
if job.break_on_newline && chr == '\n' {
|
||||
out_paragraphs.push(Paragraph::default());
|
||||
out_paragraphs.push(Paragraph::from_section_index(section_index));
|
||||
paragraph = out_paragraphs.last_mut().unwrap();
|
||||
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
|
||||
} else {
|
||||
@@ -163,8 +186,8 @@ fn rect_from_x_range(x_range: RangeInclusive<f32>) -> Rect {
|
||||
Rect::from_x_y_ranges(x_range, 0.0..=0.0)
|
||||
}
|
||||
|
||||
// Ignores the Y coordinate.
|
||||
fn rows_from_paragraphs(
|
||||
fonts: &mut FontsImpl,
|
||||
paragraphs: Vec<Paragraph>,
|
||||
job: &LayoutJob,
|
||||
elided: &mut bool,
|
||||
@@ -183,6 +206,7 @@ fn rows_from_paragraphs(
|
||||
|
||||
if paragraph.glyphs.is_empty() {
|
||||
rows.push(Row {
|
||||
section_index_at_start: paragraph.section_index_at_start,
|
||||
glyphs: vec![],
|
||||
visuals: Default::default(),
|
||||
rect: Rect::from_min_size(
|
||||
@@ -197,13 +221,14 @@ fn rows_from_paragraphs(
|
||||
// Early-out optimization: the whole paragraph fits on one row.
|
||||
let paragraph_min_x = paragraph.glyphs[0].pos.x;
|
||||
rows.push(Row {
|
||||
section_index_at_start: paragraph.section_index_at_start,
|
||||
glyphs: paragraph.glyphs,
|
||||
visuals: Default::default(),
|
||||
rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x),
|
||||
ends_with_newline: !is_last_paragraph,
|
||||
});
|
||||
} else {
|
||||
line_break(fonts, ¶graph, job, &mut rows, elided);
|
||||
line_break(¶graph, job, &mut rows, elided);
|
||||
rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph;
|
||||
}
|
||||
}
|
||||
@@ -212,13 +237,7 @@ fn rows_from_paragraphs(
|
||||
rows
|
||||
}
|
||||
|
||||
fn line_break(
|
||||
fonts: &mut FontsImpl,
|
||||
paragraph: &Paragraph,
|
||||
job: &LayoutJob,
|
||||
out_rows: &mut Vec<Row>,
|
||||
elided: &mut bool,
|
||||
) {
|
||||
fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec<Row>, elided: &mut bool) {
|
||||
// Keeps track of good places to insert row break if we exceed `wrap_width`.
|
||||
let mut row_break_candidates = RowBreakCandidates::default();
|
||||
|
||||
@@ -227,13 +246,13 @@ fn line_break(
|
||||
let mut row_start_idx = 0;
|
||||
|
||||
for i in 0..paragraph.glyphs.len() {
|
||||
let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x;
|
||||
|
||||
if job.wrap.max_rows <= out_rows.len() {
|
||||
*elided = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x;
|
||||
|
||||
if job.wrap.max_width < potential_row_width {
|
||||
// Row break:
|
||||
|
||||
@@ -243,6 +262,7 @@ fn line_break(
|
||||
// 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),
|
||||
@@ -261,10 +281,12 @@ fn line_break(
|
||||
})
|
||||
.collect();
|
||||
|
||||
let section_index_at_start = glyphs[0].section_index;
|
||||
let paragraph_min_x = glyphs[0].pos.x;
|
||||
let paragraph_max_x = glyphs.last().unwrap().max_x();
|
||||
|
||||
out_rows.push(Row {
|
||||
section_index_at_start,
|
||||
glyphs,
|
||||
visuals: Default::default(),
|
||||
rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x),
|
||||
@@ -287,10 +309,7 @@ fn line_break(
|
||||
// Final row of text:
|
||||
|
||||
if job.wrap.max_rows <= out_rows.len() {
|
||||
if let Some(last_row) = out_rows.last_mut() {
|
||||
replace_last_glyph_with_overflow_character(fonts, job, last_row);
|
||||
*elided = true;
|
||||
}
|
||||
*elided = true; // can't fit another row
|
||||
} else {
|
||||
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
|
||||
.iter()
|
||||
@@ -301,10 +320,12 @@ fn line_break(
|
||||
})
|
||||
.collect();
|
||||
|
||||
let section_index_at_start = glyphs[0].section_index;
|
||||
let paragraph_min_x = glyphs[0].pos.x;
|
||||
let paragraph_max_x = glyphs.last().unwrap().max_x();
|
||||
|
||||
out_rows.push(Row {
|
||||
section_index_at_start,
|
||||
glyphs,
|
||||
visuals: Default::default(),
|
||||
rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x),
|
||||
@@ -315,76 +336,148 @@ fn line_break(
|
||||
}
|
||||
|
||||
/// Trims the last glyphs in the row and replaces it with an overflow character (e.g. `…`).
|
||||
///
|
||||
/// Called before we have any Y coordinates.
|
||||
fn replace_last_glyph_with_overflow_character(
|
||||
fonts: &mut FontsImpl,
|
||||
job: &LayoutJob,
|
||||
row: &mut Row,
|
||||
) {
|
||||
fn row_width(row: &Row) -> f32 {
|
||||
if let (Some(first), Some(last)) = (row.glyphs.first(), row.glyphs.last()) {
|
||||
last.max_x() - first.pos.x
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn row_height(section: &LayoutSection, font: &Font) -> f32 {
|
||||
section
|
||||
.format
|
||||
.line_height
|
||||
.unwrap_or_else(|| font.row_height())
|
||||
}
|
||||
|
||||
let Some(overflow_character) = job.wrap.overflow_character else {
|
||||
return;
|
||||
};
|
||||
|
||||
// We always try to just append the character first:
|
||||
if let Some(last_glyph) = row.glyphs.last() {
|
||||
let section_index = last_glyph.section_index;
|
||||
let section = &job.sections[section_index as usize];
|
||||
let font = fonts.font(§ion.format.font_id);
|
||||
let line_height = row_height(section, font);
|
||||
|
||||
let (_, last_glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
|
||||
|
||||
let mut x = last_glyph.pos.x + last_glyph.size.x;
|
||||
|
||||
let (font_impl, replacement_glyph_info) = font.font_impl_and_glyph_info(overflow_character);
|
||||
|
||||
{
|
||||
// Kerning:
|
||||
x += section.format.extra_letter_spacing;
|
||||
if let Some(font_impl) = font_impl {
|
||||
x += font_impl.pair_kerning(last_glyph_info.id, replacement_glyph_info.id);
|
||||
}
|
||||
}
|
||||
|
||||
row.glyphs.push(Glyph {
|
||||
chr: overflow_character,
|
||||
pos: pos2(x, f32::NAN),
|
||||
size: vec2(replacement_glyph_info.advance_width, line_height),
|
||||
ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird
|
||||
uv_rect: replacement_glyph_info.uv_rect,
|
||||
section_index,
|
||||
});
|
||||
} else {
|
||||
let section_index = row.section_index_at_start;
|
||||
let section = &job.sections[section_index as usize];
|
||||
let font = fonts.font(§ion.format.font_id);
|
||||
let line_height = row_height(section, font);
|
||||
|
||||
let x = 0.0; // TODO(emilk): heed paragraph leading_space 😬
|
||||
|
||||
let (font_impl, replacement_glyph_info) = font.font_impl_and_glyph_info(overflow_character);
|
||||
|
||||
row.glyphs.push(Glyph {
|
||||
chr: overflow_character,
|
||||
pos: pos2(x, f32::NAN),
|
||||
size: vec2(replacement_glyph_info.advance_width, line_height),
|
||||
ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird
|
||||
uv_rect: replacement_glyph_info.uv_rect,
|
||||
section_index,
|
||||
});
|
||||
}
|
||||
|
||||
if row_width(row) <= job.wrap.max_width || row.glyphs.len() == 1 {
|
||||
return; // we are done
|
||||
}
|
||||
|
||||
// We didn't fit it. Remove it again…
|
||||
row.glyphs.pop();
|
||||
|
||||
// …then go into a loop where we replace the last character with the overflow character
|
||||
// until we fit within the max_width:
|
||||
|
||||
loop {
|
||||
let (prev_glyph, last_glyph) = match row.glyphs.as_mut_slice() {
|
||||
[.., prev, last] => (Some(prev), last),
|
||||
[.., last] => (None, last),
|
||||
_ => break,
|
||||
_ => {
|
||||
unreachable!("We've already explicitly handled the empty row");
|
||||
}
|
||||
};
|
||||
|
||||
let section = &job.sections[last_glyph.section_index as usize];
|
||||
let extra_letter_spacing = section.format.extra_letter_spacing;
|
||||
let font = fonts.font(§ion.format.font_id);
|
||||
let line_height = section
|
||||
.format
|
||||
.line_height
|
||||
.unwrap_or_else(|| font.row_height());
|
||||
let line_height = row_height(section, font);
|
||||
|
||||
let prev_glyph_id = prev_glyph.map(|prev_glyph| {
|
||||
let (_, prev_glyph_info) = font.font_impl_and_glyph_info(prev_glyph.chr);
|
||||
prev_glyph_info.id
|
||||
});
|
||||
if let Some(prev_glyph) = prev_glyph {
|
||||
let prev_glyph_id = font.font_impl_and_glyph_info(prev_glyph.chr).1.id;
|
||||
|
||||
// undo kerning with previous glyph
|
||||
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
|
||||
last_glyph.pos.x -= extra_letter_spacing
|
||||
+ font_impl
|
||||
.zip(prev_glyph_id)
|
||||
.map(|(font_impl, prev_glyph_id)| {
|
||||
font_impl.pair_kerning(prev_glyph_id, glyph_info.id)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Undo kerning with previous glyph:
|
||||
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
|
||||
last_glyph.pos.x -= extra_letter_spacing;
|
||||
if let Some(font_impl) = font_impl {
|
||||
last_glyph.pos.x -= font_impl.pair_kerning(prev_glyph_id, glyph_info.id);
|
||||
}
|
||||
|
||||
// replace the glyph
|
||||
last_glyph.chr = overflow_character;
|
||||
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
|
||||
last_glyph.size = vec2(glyph_info.advance_width, line_height);
|
||||
last_glyph.uv_rect = glyph_info.uv_rect;
|
||||
// Replace the glyph:
|
||||
last_glyph.chr = overflow_character;
|
||||
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
|
||||
last_glyph.size = vec2(glyph_info.advance_width, line_height);
|
||||
last_glyph.uv_rect = glyph_info.uv_rect;
|
||||
|
||||
// reapply kerning
|
||||
last_glyph.pos.x += extra_letter_spacing
|
||||
+ font_impl
|
||||
.zip(prev_glyph_id)
|
||||
.map(|(font_impl, prev_glyph_id)| {
|
||||
font_impl.pair_kerning(prev_glyph_id, glyph_info.id)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Reapply kerning:
|
||||
last_glyph.pos.x += extra_letter_spacing;
|
||||
if let Some(font_impl) = font_impl {
|
||||
last_glyph.pos.x += font_impl.pair_kerning(prev_glyph_id, glyph_info.id);
|
||||
}
|
||||
|
||||
row.rect.max.x = last_glyph.max_x();
|
||||
// Check if we're within width budget:
|
||||
if row_width(row) <= job.wrap.max_width || row.glyphs.len() == 1 {
|
||||
return; // We are done
|
||||
}
|
||||
|
||||
// check if we're within width budget
|
||||
let row_end_x = last_glyph.max_x();
|
||||
let row_start_x = row.glyphs.first().unwrap().pos.x; // if `last_mut()` returned `Some`, then so will `first()`
|
||||
let row_width = row_end_x - row_start_x;
|
||||
if row_width <= job.wrap.max_width {
|
||||
return; // we are done
|
||||
// We didn't fit - pop the last glyph and try again.
|
||||
row.glyphs.pop();
|
||||
} else {
|
||||
// Just replace and be done with it.
|
||||
last_glyph.chr = overflow_character;
|
||||
let (_, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
|
||||
last_glyph.size = vec2(glyph_info.advance_width, line_height);
|
||||
last_glyph.uv_rect = glyph_info.uv_rect;
|
||||
return;
|
||||
}
|
||||
|
||||
row.glyphs.pop();
|
||||
}
|
||||
|
||||
// We failed to insert `overflow_character` without exceeding `wrap_width`.
|
||||
}
|
||||
|
||||
/// Horizontally aligned the text on a row.
|
||||
///
|
||||
/// /// Ignores the Y coordinate.
|
||||
fn halign_and_justify_row(
|
||||
point_scale: PointScale,
|
||||
row: &mut Row,
|
||||
@@ -879,49 +972,93 @@ fn is_cjk_break_allowed(c: char) -> bool {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_zero_max_width() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section("W".into(), super::TextFormat::default());
|
||||
layout_job.wrap.max_width = 0.0;
|
||||
let galley = super::layout(&mut fonts, layout_job.into());
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{super::*, *};
|
||||
|
||||
#[test]
|
||||
fn test_cjk() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
super::TextFormat::default(),
|
||||
);
|
||||
layout_job.wrap.max_width = 90.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!["日本語と", "Englishの混在", "した文章"]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_zero_max_width() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default());
|
||||
layout_job.wrap.max_width = 0.0;
|
||||
let galley = layout(&mut fonts, layout_job.into());
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_cjk() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
super::TextFormat::default(),
|
||||
);
|
||||
layout_job.wrap.max_width = 110.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!["日本語とEnglish", "の混在した文章"]
|
||||
);
|
||||
#[test]
|
||||
fn test_truncate_with_newline() {
|
||||
// No matter where we wrap, we should be appending the newline character.
|
||||
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let text_format = TextFormat {
|
||||
font_id: FontId::monospace(12.0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for text in ["Hello\nworld", "\nfoo"] {
|
||||
for break_anywhere in [false, true] {
|
||||
for max_width in [0.0, 5.0, 10.0, 20.0, f32::INFINITY] {
|
||||
let mut layout_job =
|
||||
LayoutJob::single_section(text.into(), text_format.clone());
|
||||
layout_job.wrap.max_width = max_width;
|
||||
layout_job.wrap.max_rows = 1;
|
||||
layout_job.wrap.break_anywhere = break_anywhere;
|
||||
|
||||
let galley = layout(&mut fonts, layout_job.into());
|
||||
|
||||
assert!(galley.elided);
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
let row_text = galley.rows[0].text();
|
||||
assert!(
|
||||
row_text.ends_with('…'),
|
||||
"Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut layout_job = LayoutJob::single_section("Hello\nworld".into(), text_format);
|
||||
layout_job.wrap.max_width = 50.0;
|
||||
layout_job.wrap.max_rows = 1;
|
||||
layout_job.wrap.break_anywhere = false;
|
||||
|
||||
let galley = layout(&mut fonts, layout_job.into());
|
||||
|
||||
assert!(galley.elided);
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
let row_text = galley.rows[0].text();
|
||||
assert_eq!(row_text, "Hello…");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cjk() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
TextFormat::default(),
|
||||
);
|
||||
layout_job.wrap.max_width = 90.0;
|
||||
let galley = layout(&mut fonts, layout_job.into());
|
||||
assert_eq!(
|
||||
galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
|
||||
vec!["日本語と", "Englishの混在", "した文章"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_cjk() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
TextFormat::default(),
|
||||
);
|
||||
layout_job.wrap.max_width = 110.0;
|
||||
let galley = layout(&mut fonts, layout_job.into());
|
||||
assert_eq!(
|
||||
galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
|
||||
vec!["日本語とEnglish", "の混在した文章"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ impl Default for TextWrapping {
|
||||
}
|
||||
|
||||
impl TextWrapping {
|
||||
/// A row can be as long as it need to be
|
||||
/// A row can be as long as it need to be.
|
||||
pub fn no_max_width() -> Self {
|
||||
Self {
|
||||
max_width: f32::INFINITY,
|
||||
@@ -402,8 +402,8 @@ impl TextWrapping {
|
||||
}
|
||||
}
|
||||
|
||||
/// Elide text that doesn't fit within the given width.
|
||||
pub fn elide_at_width(max_width: f32) -> Self {
|
||||
/// Elide text that doesn't fit within the given width, replaced with `…`.
|
||||
pub fn truncate_at_width(max_width: f32) -> Self {
|
||||
Self {
|
||||
max_width,
|
||||
max_rows: 1,
|
||||
@@ -475,6 +475,9 @@ pub struct Galley {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Row {
|
||||
/// This is included in case there are no glyphs
|
||||
pub section_index_at_start: u32,
|
||||
|
||||
/// One for each `char`.
|
||||
pub glyphs: Vec<Glyph>,
|
||||
|
||||
@@ -561,6 +564,11 @@ impl Glyph {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
impl Row {
|
||||
/// The text on this row, excluding the implicit `\n` if any.
|
||||
pub fn text(&self) -> String {
|
||||
self.glyphs.iter().map(|g| g.chr).collect()
|
||||
}
|
||||
|
||||
/// Excludes the implicit `\n` after the [`Row`], if any.
|
||||
#[inline]
|
||||
pub fn char_count_excluding_newline(&self) -> usize {
|
||||
|
||||
@@ -28,7 +28,7 @@ impl eframe::App for MyApp {
|
||||
ui.horizontal(|ui| {
|
||||
ui.monospace(cmd);
|
||||
if ui.small_button("📋").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = cmd.into());
|
||||
ui.ctx().copy_text(cmd.into());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user