diff options
| -rw-r--r-- | core/src/window/event.rs | 2 | ||||
| -rw-r--r-- | graphics/src/damage.rs | 5 | ||||
| -rw-r--r-- | graphics/src/primitive.rs | 2 | ||||
| -rw-r--r-- | graphics/src/text.rs | 27 | ||||
| -rw-r--r-- | graphics/src/text/paragraph.rs | 25 | ||||
| -rw-r--r-- | renderer/src/backend.rs | 100 | ||||
| -rw-r--r-- | runtime/src/window.rs | 22 | ||||
| -rw-r--r-- | runtime/src/window/action.rs | 23 | ||||
| -rw-r--r-- | tiny_skia/src/backend.rs | 30 | ||||
| -rw-r--r-- | tiny_skia/src/text.rs | 29 | ||||
| -rw-r--r-- | wgpu/src/layer.rs | 15 | ||||
| -rw-r--r-- | wgpu/src/layer/text.rs | 5 | ||||
| -rw-r--r-- | wgpu/src/primitive/pipeline.rs | 4 | ||||
| -rw-r--r-- | wgpu/src/text.rs | 22 | ||||
| -rw-r--r-- | widget/src/lazy/helpers.rs | 3 | ||||
| -rw-r--r-- | widget/src/text_input.rs | 54 | ||||
| -rw-r--r-- | winit/src/application.rs | 10 | ||||
| -rw-r--r-- | winit/src/multi_window.rs | 14 | 
18 files changed, 252 insertions, 140 deletions
diff --git a/core/src/window/event.rs b/core/src/window/event.rs index b9ee7aca..a14d127f 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -58,7 +58,7 @@ pub enum Event {      /// for each file separately.      FileHovered(PathBuf), -    /// A file has beend dropped into the window. +    /// A file has been dropped into the window.      ///      /// When the user drops multiple files at once, this event will be emitted      /// for each file separately. diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 595cc274..59e9f5b4 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -73,6 +73,11 @@ impl<T: Damage> Damage for Primitive<T> {                  bounds.expand(1.5)              } +            Self::RawText(raw) => { +                // TODO: Add `size` field to `raw` to compute more accurate +                // damage bounds (?) +                raw.clip_bounds.expand(1.5) +            }              Self::Quad { bounds, .. }              | Self::Image { bounds, .. }              | Self::Svg { bounds, .. } => bounds.expand(1.0), diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index ed75776c..20affaaf 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -57,6 +57,8 @@ pub enum Primitive<T> {          /// The clip bounds of the editor.          clip_bounds: Rectangle,      }, +    /// A raw `cosmic-text` primitive +    RawText(crate::text::Raw),      /// A quad primitive      Quad {          /// The bounds of the quad diff --git a/graphics/src/text.rs b/graphics/src/text.rs index fc7694c2..8fd037fe 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -12,11 +12,11 @@ pub use cosmic_text;  use crate::color;  use crate::core::font::{self, Font};  use crate::core::text::Shaping; -use crate::core::{Color, Size}; +use crate::core::{Color, Point, Rectangle, Size};  use once_cell::sync::OnceCell;  use std::borrow::Cow; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, Weak};  /// Returns the global [`FontSystem`].  pub fn font_system() -> &'static RwLock<FontSystem> { @@ -68,6 +68,29 @@ impl FontSystem {  #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]  pub struct Version(u32); +/// A weak reference to a [`cosmic-text::Buffer`] that can be drawn. +#[derive(Debug, Clone)] +pub struct Raw { +    /// A weak reference to a [`cosmic_text::Buffer`]. +    pub buffer: Weak<cosmic_text::Buffer>, +    /// The position of the text. +    pub position: Point, +    /// The color of the text. +    pub color: Color, +    /// The clip bounds of the text. +    pub clip_bounds: Rectangle, +} + +impl PartialEq for Raw { +    fn eq(&self, _other: &Self) -> bool { +        // TODO: There is no proper way to compare raw buffers +        // For now, no two instances of `Raw` text will be equal. +        // This should be fine, but could trigger unnecessary redraws +        // in the future. +        false +    } +} +  /// Measures the dimensions of the given [`cosmic_text::Buffer`].  pub fn measure(buffer: &cosmic_text::Buffer) -> Size {      let (width, total_lines) = buffer diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 4a08a8f4..5d027542 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -187,38 +187,43 @@ impl core::text::Paragraph for Paragraph {      }      fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> { +        use unicode_segmentation::UnicodeSegmentation; +          let run = self.internal().buffer.layout_runs().nth(line)?;          // index represents a grapheme, not a glyph          // Let's find the first glyph for the given grapheme cluster          let mut last_start = None; +        let mut last_grapheme_count = 0;          let mut graphemes_seen = 0;          let glyph = run              .glyphs              .iter()              .find(|glyph| { -                if graphemes_seen == index { -                    return true; -                } -                  if Some(glyph.start) != last_start { +                    last_grapheme_count = run.text[glyph.start..glyph.end] +                        .graphemes(false) +                        .count();                      last_start = Some(glyph.start); -                    graphemes_seen += 1; +                    graphemes_seen += last_grapheme_count;                  } -                false +                graphemes_seen >= index              })              .or_else(|| run.glyphs.last())?; -        let advance_last = if index == run.glyphs.len() { -            glyph.w -        } else { +        let advance = if index == 0 {              0.0 +        } else { +            glyph.w +                * (1.0 +                    - graphemes_seen.saturating_sub(index) as f32 +                        / last_grapheme_count.max(1) as f32)          };          Some(Point::new( -            glyph.x + glyph.x_offset * glyph.font_size + advance_last, +            glyph.x + glyph.x_offset * glyph.font_size + advance,              glyph.y - glyph.y_offset * glyph.font_size,          ))      } diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs deleted file mode 100644 index 3f229b52..00000000 --- a/renderer/src/backend.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::core::text; -use crate::core::{Font, Point, Size}; -use crate::graphics::backend; - -use std::borrow::Cow; - -#[allow(clippy::large_enum_variant)] -pub enum Backend { -    TinySkia(iced_tiny_skia::Backend), -    #[cfg(feature = "wgpu")] -    Wgpu(iced_wgpu::Backend), -} - -macro_rules! delegate { -    ($backend:expr, $name:ident, $body:expr) => { -        match $backend { -            Self::TinySkia($name) => $body, -            #[cfg(feature = "wgpu")] -            Self::Wgpu($name) => $body, -        } -    }; -} - -impl backend::Text for Backend { -    const ICON_FONT: Font = Font::with_name("Iced-Icons"); -    const CHECKMARK_ICON: char = '\u{f00c}'; -    const ARROW_DOWN_ICON: char = '\u{e800}'; - -    fn default_font(&self) -> Font { -        delegate!(self, backend, backend.default_font()) -    } - -    fn default_size(&self) -> f32 { -        delegate!(self, backend, backend.default_size()) -    } - -    fn measure( -        &self, -        contents: &str, -        size: f32, -        line_height: text::LineHeight, -        font: Font, -        bounds: Size, -        shaping: text::Shaping, -    ) -> Size { -        delegate!( -            self, -            backend, -            backend.measure(contents, size, line_height, font, bounds, shaping) -        ) -    } - -    fn hit_test( -        &self, -        contents: &str, -        size: f32, -        line_height: text::LineHeight, -        font: Font, -        bounds: Size, -        shaping: text::Shaping, -        position: Point, -        nearest_only: bool, -    ) -> Option<text::Hit> { -        delegate!( -            self, -            backend, -            backend.hit_test( -                contents, -                size, -                line_height, -                font, -                bounds, -                shaping, -                position, -                nearest_only, -            ) -        ) -    } - -    fn load_font(&mut self, font: Cow<'static, [u8]>) { -        delegate!(self, backend, backend.load_font(font)); -    } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { -    fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { -        delegate!(self, backend, backend.dimensions(handle)) -    } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { -    fn viewport_dimensions( -        &self, -        handle: &crate::core::svg::Handle, -    ) -> Size<u32> { -        delegate!(self, backend, backend.viewport_dimensions(handle)) -    } -} diff --git a/runtime/src/window.rs b/runtime/src/window.rs index f9d943f6..2136d64d 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -65,11 +65,33 @@ pub fn fetch_size<Message>(      Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f))))  } +/// Fetches if the window is maximized. +pub fn fetch_maximized<Message>( +    id: Id, +    f: impl FnOnce(bool) -> Message + 'static, +) -> Command<Message> { +    Command::single(command::Action::Window(Action::FetchMaximized( +        id, +        Box::new(f), +    ))) +} +  /// Maximizes the window.  pub fn maximize<Message>(id: Id, maximized: bool) -> Command<Message> {      Command::single(command::Action::Window(Action::Maximize(id, maximized)))  } +/// Fetches if the window is minimized. +pub fn fetch_minimized<Message>( +    id: Id, +    f: impl FnOnce(Option<bool>) -> Message + 'static, +) -> Command<Message> { +    Command::single(command::Action::Window(Action::FetchMinimized( +        id, +        Box::new(f), +    ))) +} +  /// Minimizes the window.  pub fn minimize<Message>(id: Id, minimized: bool) -> Command<Message> {      Command::single(command::Action::Window(Action::Minimize(id, minimized))) diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index 2d98b607..8b532569 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -21,8 +21,19 @@ pub enum Action<T> {      Resize(Id, Size),      /// Fetch the current logical dimensions of the window.      FetchSize(Id, Box<dyn FnOnce(Size) -> T + 'static>), +    /// Fetch if the current window is maximized or not. +    /// +    /// ## Platform-specific +    /// - **iOS / Android / Web:** Unsupported. +    FetchMaximized(Id, Box<dyn FnOnce(bool) -> T + 'static>),      /// Set the window to maximized or back      Maximize(Id, bool), +    /// Fetch if the current window is minimized or not. +    /// +    /// ## Platform-specific +    /// - **Wayland:** Always `None`. +    /// - **iOS / Android / Web:** Unsupported. +    FetchMinimized(Id, Box<dyn FnOnce(Option<bool>) -> T + 'static>),      /// Set the window to minimized or back      Minimize(Id, bool),      /// Move the window to the given logical coordinates. @@ -106,7 +117,13 @@ impl<T> Action<T> {              Self::FetchSize(id, o) => {                  Action::FetchSize(id, Box::new(move |s| f(o(s))))              } +            Self::FetchMaximized(id, o) => { +                Action::FetchMaximized(id, Box::new(move |s| f(o(s)))) +            }              Self::Maximize(id, maximized) => Action::Maximize(id, maximized), +            Self::FetchMinimized(id, o) => { +                Action::FetchMinimized(id, Box::new(move |s| f(o(s)))) +            }              Self::Minimize(id, minimized) => Action::Minimize(id, minimized),              Self::Move(id, position) => Action::Move(id, position),              Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), @@ -144,9 +161,15 @@ impl<T> fmt::Debug for Action<T> {                  write!(f, "Action::Resize({id:?}, {size:?})")              }              Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"), +            Self::FetchMaximized(id, _) => { +                write!(f, "Action::FetchMaximized({id:?})") +            }              Self::Maximize(id, maximized) => {                  write!(f, "Action::Maximize({id:?}, {maximized})")              } +            Self::FetchMinimized(id, _) => { +                write!(f, "Action::FetchMinimized({id:?})") +            }              Self::Minimize(id, minimized) => {                  write!(f, "Action::Minimize({id:?}, {minimized}")              } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 3e9bd2a5..706db40e 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,5 +1,6 @@  use crate::core::{Background, Color, Gradient, Rectangle, Vector};  use crate::graphics::backend; +use crate::graphics::text;  use crate::graphics::Viewport;  use crate::primitive::{self, Primitive}; @@ -444,6 +445,35 @@ impl Backend {                      clip_mask,                  );              } +            Primitive::RawText(text::Raw { +                buffer, +                position, +                color, +                clip_bounds: text_clip_bounds, +            }) => { +                let Some(buffer) = buffer.upgrade() else { +                    return; +                }; + +                let physical_bounds = +                    (*text_clip_bounds + translation) * scale_factor; + +                if !clip_bounds.intersects(&physical_bounds) { +                    return; +                } + +                let clip_mask = (!physical_bounds.is_within(&clip_bounds)) +                    .then_some(clip_mask as &_); + +                self.text_pipeline.draw_raw( +                    &buffer, +                    *position + translation, +                    *color, +                    scale_factor, +                    pixels, +                    clip_mask, +                ); +            }              #[cfg(feature = "image")]              Primitive::Image {                  handle, diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 70e95d01..a5a0a1b6 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,6 +1,6 @@  use crate::core::alignment;  use crate::core::text::{LineHeight, Shaping}; -use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::core::{Color, Font, Pixels, Point, Rectangle, Size};  use crate::graphics::color;  use crate::graphics::text::cache::{self, Cache};  use crate::graphics::text::editor; @@ -149,6 +149,33 @@ impl Pipeline {          );      } +    pub fn draw_raw( +        &mut self, +        buffer: &cosmic_text::Buffer, +        position: Point, +        color: Color, +        scale_factor: f32, +        pixels: &mut tiny_skia::PixmapMut<'_>, +        clip_mask: Option<&tiny_skia::Mask>, +    ) { +        let mut font_system = font_system().write().expect("Write font system"); + +        let (width, height) = buffer.size(); + +        draw( +            font_system.raw(), +            &mut self.glyph_cache, +            buffer, +            Rectangle::new(position, Size::new(width, height)), +            color, +            alignment::Horizontal::Left, +            alignment::Vertical::Top, +            scale_factor, +            pixels, +            clip_mask, +        ); +    } +      pub fn trim_cache(&mut self) {          self.cache.get_mut().trim();          self.glyph_cache.trim(); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 557a7633..4ad12a88 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -177,6 +177,21 @@ impl<'a> Layer<'a> {                      clip_bounds: *clip_bounds + translation,                  }));              } +            graphics::Primitive::RawText(graphics::text::Raw { +                buffer, +                position, +                color, +                clip_bounds, +            }) => { +                let layer = &mut layers[current_layer]; + +                layer.text.push(Text::Raw(graphics::text::Raw { +                    buffer: buffer.clone(), +                    position: *position + translation, +                    color: *color, +                    clip_bounds: *clip_bounds + translation, +                })); +            }              Primitive::Quad {                  bounds,                  background, diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index df2f2875..37ee5247 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -1,6 +1,7 @@  use crate::core::alignment;  use crate::core::text;  use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics;  use crate::graphics::text::editor;  use crate::graphics::text::paragraph; @@ -23,8 +24,10 @@ pub enum Text<'a> {          color: Color,          clip_bounds: Rectangle,      }, -    /// A cached text. +    /// Some cached text.      Cached(Cached<'a>), +    /// Some raw text. +    Raw(graphics::text::Raw),  }  #[derive(Debug, Clone)] diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs index 302e38f6..c8e45458 100644 --- a/wgpu/src/primitive/pipeline.rs +++ b/wgpu/src/primitive/pipeline.rs @@ -82,7 +82,7 @@ impl<Theme> Renderer for crate::Renderer<Theme> {  /// Stores custom, user-provided pipelines.  #[derive(Default, Debug)]  pub struct Storage { -    pipelines: HashMap<TypeId, Box<dyn Any>>, +    pipelines: HashMap<TypeId, Box<dyn Any + Send>>,  }  impl Storage { @@ -92,7 +92,7 @@ impl Storage {      }      /// Inserts the pipeline `T` in to [`Storage`]. -    pub fn store<T: 'static>(&mut self, pipeline: T) { +    pub fn store<T: 'static + Send>(&mut self, pipeline: T) {          let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline));      } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 888b1924..dca09cb8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -7,6 +7,7 @@ use crate::layer::Text;  use std::borrow::Cow;  use std::cell::RefCell; +use std::sync::Arc;  #[allow(missing_debug_implementations)]  pub struct Pipeline { @@ -76,6 +77,7 @@ impl Pipeline {              Paragraph(Paragraph),              Editor(Editor),              Cache(cache::KeyHash), +            Raw(Arc<glyphon::Buffer>),          }          let allocations: Vec<_> = sections @@ -107,6 +109,7 @@ impl Pipeline {                      Some(Allocation::Cache(key))                  } +                Text::Raw(text) => text.buffer.upgrade().map(Allocation::Raw),              })              .collect(); @@ -185,6 +188,25 @@ impl Pipeline {                              text.clip_bounds,                          )                      } +                    Text::Raw(text) => { +                        let Some(Allocation::Raw(buffer)) = allocation else { +                            return None; +                        }; + +                        let (width, height) = buffer.size(); + +                        ( +                            buffer.as_ref(), +                            Rectangle::new( +                                text.position, +                                Size::new(width, height), +                            ), +                            alignment::Horizontal::Left, +                            alignment::Vertical::Top, +                            text.color, +                            text.clip_bounds, +                        ) +                    }                  };                  let bounds = bounds * scale_factor; diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index 8ca9cb86..5dc60d52 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -6,6 +6,7 @@ use std::hash::Hash;  /// Creates a new [`Lazy`] widget with the given data `Dependency` and a  /// closure that can turn this data into a widget tree. +#[cfg(feature = "lazy")]  pub fn lazy<'a, Message, Renderer, Dependency, View>(      dependency: Dependency,      view: impl Fn(&Dependency) -> View + 'a, @@ -19,6 +20,7 @@ where  /// Turns an implementor of [`Component`] into an [`Element`] that can be  /// embedded in any application. +#[cfg(feature = "lazy")]  pub fn component<'a, C, Message, Renderer>(      component: C,  ) -> Element<'a, Message, Renderer> @@ -37,6 +39,7 @@ where  /// The `view` closure will be provided with the current [`Size`] of  /// the [`Responsive`] widget and, therefore, can be used to build the  /// contents of the widget in a responsive way. +#[cfg(feature = "lazy")]  pub fn responsive<'a, Message, Renderer>(      f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a,  ) -> Responsive<'a, Message, Renderer> diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index d8540658..3be9b8e6 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1190,31 +1190,39 @@ pub fn draw<Renderer>(          (None, 0.0)      }; -    if let Some((cursor, color)) = cursor { -        renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { -            renderer.fill_quad(cursor, color); -        }); +    let draw = |renderer: &mut Renderer, viewport| { +        if let Some((cursor, color)) = cursor { +            renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { +                renderer.fill_quad(cursor, color); +            }); +        } else { +            renderer.with_translation(Vector::ZERO, |_| {}); +        } + +        renderer.fill_paragraph( +            if text.is_empty() { +                &state.placeholder +            } else { +                &state.value +            }, +            Point::new(text_bounds.x, text_bounds.center_y()) +                - Vector::new(offset, 0.0), +            if text.is_empty() { +                theme.placeholder_color(style) +            } else if is_disabled { +                theme.disabled_color(style) +            } else { +                theme.value_color(style) +            }, +            viewport, +        ); +    }; + +    if cursor.is_some() { +        renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport));      } else { -        renderer.with_translation(Vector::ZERO, |_| {}); +        draw(renderer, text_bounds);      } - -    renderer.fill_paragraph( -        if text.is_empty() { -            &state.placeholder -        } else { -            &state.value -        }, -        Point::new(text_bounds.x, text_bounds.center_y()) -            - Vector::new(offset, 0.0), -        if text.is_empty() { -            theme.placeholder_color(style) -        } else if is_disabled { -            theme.disabled_color(style) -        } else { -            theme.value_color(style) -        }, -        text_bounds, -    );  }  /// Computes the current [`mouse::Interaction`] of the [`TextInput`]. diff --git a/winit/src/application.rs b/winit/src/application.rs index d9700075..35a35872 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -742,9 +742,19 @@ pub fn run_command<A, C, E>(                          )))                          .expect("Send message to event loop");                  } +                window::Action::FetchMaximized(_id, callback) => { +                    proxy +                        .send_event(callback(window.is_maximized())) +                        .expect("Send message to event loop"); +                }                  window::Action::Maximize(_id, maximized) => {                      window.set_maximized(maximized);                  } +                window::Action::FetchMinimized(_id, callback) => { +                    proxy +                        .send_event(callback(window.is_minimized())) +                        .expect("Send message to event loop"); +                }                  window::Action::Minimize(_id, minimized) => {                      window.set_minimized(minimized);                  } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 84651d40..1550b94b 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -942,11 +942,25 @@ fn run_command<A, C, E>(                              .expect("Send message to event loop");                      }                  } +                window::Action::FetchMaximized(id, callback) => { +                    if let Some(window) = window_manager.get_mut(id) { +                        proxy +                            .send_event(callback(window.raw.is_maximized())) +                            .expect("Send message to event loop"); +                    } +                }                  window::Action::Maximize(id, maximized) => {                      if let Some(window) = window_manager.get_mut(id) {                          window.raw.set_maximized(maximized);                      }                  } +                window::Action::FetchMinimized(id, callback) => { +                    if let Some(window) = window_manager.get_mut(id) { +                        proxy +                            .send_event(callback(window.raw.is_minimized())) +                            .expect("Send message to event loop"); +                    } +                }                  window::Action::Minimize(id, minimized) => {                      if let Some(window) = window_manager.get_mut(id) {                          window.raw.set_minimized(minimized);  | 
