1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00
Files
egui/crates/epaint/src/image.rs
Konkitoman 83aa3109d3 Multiple viewports/windows (#3172)
* Closes #1044

---
(new PR description written by @emilk)

## Overview
This PR introduces the concept of `Viewports`, which on the native
eframe backend corresponds to native OS windows.

You can spawn a new viewport using `Context::show_viewport` and
`Cotext::show_viewport_immediate`.
These needs to be called every frame the viewport should be visible.

This is implemented by the native `eframe` backend, but not the web one.

## Viewport classes
The viewports form a tree of parent-child relationships.

There are different classes of viewports.

### Root vieport
The root viewport is the original viewport, and cannot be closed without
closing the application.

### Deferred viewports
These are created with `Context::show_viewport`.
Deferred viewports take a closure that is called by the integration at a
later time, perhaps multiple times.
Deferred viewports are repainted independenantly of the parent viewport.
This means communication with them need to done via channels, or
`Arc/Mutex`.

This is the most performant type of child viewport, though a bit more
cumbersome to work with compared to immediate viewports.

### Immediate viewports
These are created with `Context::show_viewport_immediate`.
Immediate viewports take a `FnOnce` closure, similar to other egui
functions, and is called immediately. This makes communication with them
much simpler than with deferred viewports, but this simplicity comes at
a cost: whenever tha parent viewports needs to be repainted, so will the
child viewport, and vice versa. This means that if you have `N`
viewports you are poentially doing `N` times as much CPU work. However,
if all your viewports are showing animations, and thus are repainting
constantly anyway, this doesn't matter.

In short: immediate viewports are simpler to use, but can waste a lot of
CPU time.

### Embedded viewports
These are not real, independenant viewports, but is a fallback mode for
when the integration does not support real viewports. In your callback
is called with `ViewportClass::Embedded` it means you need to create an
`egui::Window` to wrap your ui in, which will then be embedded in the
parent viewport, unable to escape it.


## Using the viewports
Only one viewport is active at any one time, identified wth
`Context::viewport_id`.
You can send commands to other viewports using
`Context::send_viewport_command_to`.

There is an example in
<https://github.com/emilk/egui/tree/master/examples/multiple_viewports/src/main.rs>.

## For integrations
There are several changes relevant to integrations.

* There is a [`crate::RawInput::viewport`] with information about the
current viewport.
* The repaint callback set by `Context::set_request_repaint_callback`
now points to which viewport should be repainted.
* `Context::run` now returns a list of viewports in `FullOutput` which
should result in their own independant windows
* There is a new `Context::set_immediate_viewport_renderer` for setting
up the immediate viewport integration
* If you support viewports, you need to call
`Context::set_embed_viewports(false)`, or all new viewports will be
embedded (the default behavior).


## Future work
* Make it easy to wrap child viewports in the same chrome as
`egui::Window`
* Automatically show embedded viewports using `egui::Window`
* Use the new `ViewportBuilder` in `eframe::NativeOptions`
* Automatically position new viewport windows (they currently cover each
other)
* Add a `Context` method for listing all existing viewports

Find more at https://github.com/emilk/egui/issues/3556




---

<details>
<summary>
Outdated PR description by @konkitoman
</summary>


## Inspiration
- Godot because the app always work desktop or single_window because of
embedding
- Dear ImGui viewport system

## What is a Viewport

A Viewport is a egui isolated component!
Can be used by the egui integration to create native windows!

When you create a Viewport is possible that the backend do not supports
that!
So you need to check if the Viewport was created or you are in the
normal egui context!
This is how you can do that:
```rust
if ctx.viewport_id() != ctx.parent_viewport_id() {
    // In here you add the code for the viewport context, like
    egui::CentralPanel::default().show(ctx, |ui|{
        ui.label("This is in a native window!");
    });
}else{
    // In here you add the code for when viewport cannot be created!
   // You cannot use CentralPanel in here because you will override the app CentralPanel
   egui::Window::new("Virtual Viewport").show(ctx, |ui|{
       ui.label("This is without a native window!\nThis is in a embedded viewport");
   });
}
```

This PR do not support for drag and drop between Viewports!

After this PR is accepted i will begin work to intregrate the Viewport
system in `egui::Window`!
The `egui::Window` i want to behave the same on desktop and web
The `egui::Window` will be like Godot Window

## Changes and new

These are only public structs and functions!

<details>
<summary>

## New
</summary>

- `egui::ViewportId`
- `egui::ViewportBuilder`
This is like winit WindowBuilder

- `egui::ViewportCommand`
With this you can set any winit property on a viewport, when is a native
window!

- `egui::Context::new`
- `egui::Context::create_viewport`
- `egui::Context::create_viewport_sync`
- `egui::Context::viewport_id`
- `egui::Context::parent_viewport_id`
- `egui::Context::viewport_id_pair`
- `egui::Context::set_render_sync_callback`
- `egui::Context::is_desktop`
- `egui::Context::force_embedding`
- `egui::Context::set_force_embedding`
- `egui::Context::viewport_command`
- `egui::Context::send_viewport_command_to`
- `egui::Context::input_for`
- `egui::Context::input_mut_for`
- `egui::Context::frame_nr_for`
- `egui::Context::request_repaint_for`
- `egui::Context::request_repaint_after_for`
- `egui::Context::requested_repaint_last_frame`
- `egui::Context::requested_repaint_last_frame_for`
- `egui::Context::requested_repaint`
- `egui::Context::requested_repaint_for`
- `egui::Context::inner_rect`
- `egui::Context::outer_rect`

- `egui::InputState::inner_rect`
- `egui::InputState::outer_rect`

- `egui::WindowEvent`

</details>

<details>
<summary>

## Changes
</summary>

- `egui::Context::run`
Now needs the viewport that we want to render!

- `egui::Context::begin_frame`
Now needs the viewport that we want to render!

- `egui::Context::tessellate`
Now needs the viewport that we want to render!

- `egui::FullOutput`
```diff
- repaint_after
+ viewports
+ viewport_commands
```

- `egui::RawInput`
```diff
+ inner_rect
+ outer_rect
```

- `egui::Event`
```diff
+ WindowEvent
```
</details>

### Async Viewport

Async means that is independent from other viewports!

Is created by `egui::Context::create_viewport`

To be used you will need to wrap your state in `Arc<RwLock<T>>`
Look at viewports example to understand how to use it!

### Sync Viewport

Sync means that is dependent on his parent!

Is created by `egui::Context::create_viewport_sync`

This will pause the parent then render itself the resumes his parent!

#### ⚠️ This currently will make the fps/2 for every sync
viewport

### Common

#### ⚠️ Attention

You will need to do this when you render your content
```rust
ctx.create_viewport(ViewportBuilder::new("Simple Viewport"), | ctx | {
    let content = |ui: &mut egui::Ui|{
        ui.label("Content");
    };

    // This will make the content a popup if cannot create a native window
    if ctx.viewport_id() != ctx.parent_viewport_id() {
        egui::CentralPanel::default().show(ctx, content);
    } else {
        egui::Area::new("Simple Viewport").show(ctx, |ui| {
            egui::Frame::popup(ui.style()).show(ui, content);
        });
    };
});
````

## What you need to know as egui user

### If you are using eframe

You don't need to change anything!

### If you have a manual implementation

Now `egui::run` or `egui::begin` and `egui::tessellate` will need the
current viewport id!
You cannot create a `ViewportId` only `ViewportId::MAIN`

If you make a single window app you will set the viewport id to be
`egui::ViewportId::MAIN` or see the `examples/pure_glow`
If you want to have multiples window support look at `crates/eframe`
glow or wgpu implementations!

## If you want to try this

- cargo run -p viewports

## This before was wanted to change

This will probably be in feature PR's

### egui::Window

To create a native window when embedded was set to false
You can try that in viewports example before:
[78a0ae8](78a0ae879e)

### egui popups, context_menu, tooltip

To be a native window

</details>

---------

Co-authored-by: Konkitoman <konkitoman@users.noreply.github.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: Pablo Sichert <mail@pablosichert.com>
2023-11-16 11:25:05 +01:00

405 lines
12 KiB
Rust

use crate::{textures::TextureOptions, Color32};
use std::sync::Arc;
/// An image stored in RAM.
///
/// To load an image file, see [`ColorImage::from_rgba_unmultiplied`].
///
/// In order to paint the image on screen, you first need to convert it to
///
/// See also: [`ColorImage`], [`FontImage`].
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageData {
/// RGBA image.
Color(Arc<ColorImage>),
/// Used for the font texture.
Font(FontImage),
}
impl ImageData {
pub fn size(&self) -> [usize; 2] {
match self {
Self::Color(image) => image.size,
Self::Font(image) => image.size,
}
}
pub fn width(&self) -> usize {
self.size()[0]
}
pub fn height(&self) -> usize {
self.size()[1]
}
pub fn bytes_per_pixel(&self) -> usize {
match self {
Self::Color(_) | Self::Font(_) => 4,
}
}
}
// ----------------------------------------------------------------------------
/// A 2D RGBA color image in RAM.
#[derive(Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ColorImage {
/// width, height.
pub size: [usize; 2],
/// The pixels, row by row, from top to bottom.
pub pixels: Vec<Color32>,
}
impl ColorImage {
/// Create an image filled with the given color.
pub fn new(size: [usize; 2], color: Color32) -> Self {
Self {
size,
pixels: vec![color; size[0] * size[1]],
}
}
/// Create a [`ColorImage`] from flat un-multiplied RGBA data.
///
/// This is usually what you want to use after having loaded an image file.
///
/// Panics if `size[0] * size[1] * 4 != rgba.len()`.
///
/// ## Example using the [`image`](crates.io/crates/image) crate:
/// ``` ignore
/// fn load_image_from_path(path: &std::path::Path) -> Result<egui::ColorImage, image::ImageError> {
/// let image = image::io::Reader::open(path)?.decode()?;
/// let size = [image.width() as _, image.height() as _];
/// let image_buffer = image.to_rgba8();
/// let pixels = image_buffer.as_flat_samples();
/// Ok(egui::ColorImage::from_rgba_unmultiplied(
/// size,
/// pixels.as_slice(),
/// ))
/// }
///
/// fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
/// let image = image::load_from_memory(image_data)?;
/// let size = [image.width() as _, image.height() as _];
/// let image_buffer = image.to_rgba8();
/// let pixels = image_buffer.as_flat_samples();
/// Ok(ColorImage::from_rgba_unmultiplied(
/// size,
/// pixels.as_slice(),
/// ))
/// }
/// ```
pub fn from_rgba_unmultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
assert_eq!(size[0] * size[1] * 4, rgba.len());
let pixels = rgba
.chunks_exact(4)
.map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
.collect();
Self { size, pixels }
}
pub fn from_rgba_premultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
assert_eq!(size[0] * size[1] * 4, rgba.len());
let pixels = rgba
.chunks_exact(4)
.map(|p| Color32::from_rgba_premultiplied(p[0], p[1], p[2], p[3]))
.collect();
Self { size, pixels }
}
/// Create a [`ColorImage`] from flat opaque gray data.
///
/// Panics if `size[0] * size[1] != gray.len()`.
pub fn from_gray(size: [usize; 2], gray: &[u8]) -> Self {
assert_eq!(size[0] * size[1], gray.len());
let pixels = gray.iter().map(|p| Color32::from_gray(*p)).collect();
Self { size, pixels }
}
/// A view of the underlying data as `&[u8]`
#[cfg(feature = "bytemuck")]
pub fn as_raw(&self) -> &[u8] {
bytemuck::cast_slice(&self.pixels)
}
/// A view of the underlying data as `&mut [u8]`
#[cfg(feature = "bytemuck")]
pub fn as_raw_mut(&mut self) -> &mut [u8] {
bytemuck::cast_slice_mut(&mut self.pixels)
}
/// Create a new Image from a patch of the current image. This method is especially convenient for screenshotting a part of the app
/// since `region` can be interpreted as screen coordinates of the entire screenshot if `pixels_per_point` is provided for the native application.
/// The floats of [`emath::Rect`] are cast to usize, rounding them down in order to interpret them as indices to the image data.
///
/// Panics if `region.min.x > region.max.x || region.min.y > region.max.y`, or if a region larger than the image is passed.
pub fn region(&self, region: &emath::Rect, pixels_per_point: Option<f32>) -> Self {
let pixels_per_point = pixels_per_point.unwrap_or(1.0);
let min_x = (region.min.x * pixels_per_point) as usize;
let max_x = (region.max.x * pixels_per_point) as usize;
let min_y = (region.min.y * pixels_per_point) as usize;
let max_y = (region.max.y * pixels_per_point) as usize;
assert!(min_x <= max_x);
assert!(min_y <= max_y);
let width = max_x - min_x;
let height = max_y - min_y;
let mut output = Vec::with_capacity(width * height);
let row_stride = self.size[0];
for row in min_y..max_y {
output.extend_from_slice(
&self.pixels[row * row_stride + min_x..row * row_stride + max_x],
);
}
Self {
size: [width, height],
pixels: output,
}
}
/// Create a [`ColorImage`] from flat RGB data.
///
/// This is what you want to use after having loaded an image file (and if
/// you are ignoring the alpha channel - considering it to always be 0xff)
///
/// Panics if `size[0] * size[1] * 3 != rgb.len()`.
pub fn from_rgb(size: [usize; 2], rgb: &[u8]) -> Self {
assert_eq!(size[0] * size[1] * 3, rgb.len());
let pixels = rgb
.chunks_exact(3)
.map(|p| Color32::from_rgb(p[0], p[1], p[2]))
.collect();
Self { size, pixels }
}
/// An example color image, useful for tests.
pub fn example() -> Self {
let width = 128;
let height = 64;
let mut img = Self::new([width, height], Color32::TRANSPARENT);
for y in 0..height {
for x in 0..width {
let h = x as f32 / width as f32;
let s = 1.0;
let v = 1.0;
let a = y as f32 / height as f32;
img[(x, y)] = crate::Hsva { h, s, v, a }.into();
}
}
img
}
#[inline]
pub fn width(&self) -> usize {
self.size[0]
}
#[inline]
pub fn height(&self) -> usize {
self.size[1]
}
}
impl std::ops::Index<(usize, usize)> for ColorImage {
type Output = Color32;
#[inline]
fn index(&self, (x, y): (usize, usize)) -> &Color32 {
let [w, h] = self.size;
assert!(x < w && y < h);
&self.pixels[y * w + x]
}
}
impl std::ops::IndexMut<(usize, usize)> for ColorImage {
#[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Color32 {
let [w, h] = self.size;
assert!(x < w && y < h);
&mut self.pixels[y * w + x]
}
}
impl From<ColorImage> for ImageData {
#[inline(always)]
fn from(image: ColorImage) -> Self {
Self::Color(Arc::new(image))
}
}
impl From<Arc<ColorImage>> for ImageData {
#[inline]
fn from(image: Arc<ColorImage>) -> Self {
Self::Color(image)
}
}
impl std::fmt::Debug for ColorImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ColorImage")
.field("size", &self.size)
.field("pixel-count", &self.pixels.len())
.finish_non_exhaustive()
}
}
// ----------------------------------------------------------------------------
/// A single-channel image designed for the font texture.
///
/// Each value represents "coverage", i.e. how much a texel is covered by a character.
///
/// This is roughly interpreted as the opacity of a white image.
#[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FontImage {
/// width, height
pub size: [usize; 2],
/// The coverage value.
///
/// Often you want to use [`Self::srgba_pixels`] instead.
pub pixels: Vec<f32>,
}
impl FontImage {
pub fn new(size: [usize; 2]) -> Self {
Self {
size,
pixels: vec![0.0; size[0] * size[1]],
}
}
#[inline]
pub fn width(&self) -> usize {
self.size[0]
}
#[inline]
pub fn height(&self) -> usize {
self.size[1]
}
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
///
/// `gamma` should normally be set to `None`.
///
/// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`.
#[inline]
pub fn srgba_pixels(
&'_ self,
gamma: Option<f32>,
) -> impl ExactSizeIterator<Item = Color32> + '_ {
let gamma = gamma.unwrap_or(0.55); // TODO(emilk): this default coverage gamma is a magic constant, chosen by eye. I don't even know why we need it.
self.pixels.iter().map(move |coverage| {
let alpha = coverage.powf(gamma);
// We want to multiply with `vec4(alpha)` in the fragment shader:
let a = fast_round(alpha * 255.0);
Color32::from_rgba_premultiplied(a, a, a, a)
})
}
/// Clone a sub-region as a new image.
pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> FontImage {
assert!(x + w <= self.width());
assert!(y + h <= self.height());
let mut pixels = Vec::with_capacity(w * h);
for y in y..y + h {
let offset = y * self.width() + x;
pixels.extend(&self.pixels[offset..(offset + w)]);
}
assert_eq!(pixels.len(), w * h);
FontImage {
size: [w, h],
pixels,
}
}
}
impl std::ops::Index<(usize, usize)> for FontImage {
type Output = f32;
#[inline]
fn index(&self, (x, y): (usize, usize)) -> &f32 {
let [w, h] = self.size;
assert!(x < w && y < h);
&self.pixels[y * w + x]
}
}
impl std::ops::IndexMut<(usize, usize)> for FontImage {
#[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 {
let [w, h] = self.size;
assert!(x < w && y < h);
&mut self.pixels[y * w + x]
}
}
impl From<FontImage> for ImageData {
#[inline(always)]
fn from(image: FontImage) -> Self {
Self::Font(image)
}
}
#[inline]
fn fast_round(r: f32) -> u8 {
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
}
// ----------------------------------------------------------------------------
/// A change to an image.
///
/// Either a whole new image, or an update to a rectangular region of it.
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[must_use = "The painter must take care of this"]
pub struct ImageDelta {
/// What to set the texture to.
///
/// If [`Self::pos`] is `None`, this describes the whole texture.
///
/// If [`Self::pos`] is `Some`, this describes a patch of the whole image starting at [`Self::pos`].
pub image: ImageData,
pub options: TextureOptions,
/// If `None`, set the whole texture to [`Self::image`].
///
/// If `Some(pos)`, update a sub-region of an already allocated texture with the patch in [`Self::image`].
pub pos: Option<[usize; 2]>,
}
impl ImageDelta {
/// Update the whole texture.
pub fn full(image: impl Into<ImageData>, options: TextureOptions) -> Self {
Self {
image: image.into(),
options,
pos: None,
}
}
/// Update a sub-region of an existing texture.
pub fn partial(pos: [usize; 2], image: impl Into<ImageData>, options: TextureOptions) -> Self {
Self {
image: image.into(),
options,
pos: Some(pos),
}
}
/// Is this affecting the whole texture?
/// If `false`, this is a partial (sub-region) update.
pub fn is_whole(&self) -> bool {
self.pos.is_none()
}
}