diff options
Diffstat (limited to 'native')
| -rw-r--r-- | native/Cargo.toml | 1 | ||||
| -rw-r--r-- | native/src/subscription.rs | 91 | ||||
| -rw-r--r-- | native/src/widget/checkbox.rs | 22 | ||||
| -rw-r--r-- | native/src/widget/helpers.rs | 5 | ||||
| -rw-r--r-- | native/src/widget/radio.rs | 37 | ||||
| -rw-r--r-- | native/src/widget/slider.rs | 88 | ||||
| -rw-r--r-- | native/src/widget/text_input.rs | 202 | ||||
| -rw-r--r-- | native/src/widget/vertical_slider.rs | 86 | ||||
| -rw-r--r-- | native/src/window.rs | 3 | ||||
| -rw-r--r-- | native/src/window/action.rs | 21 | ||||
| -rw-r--r-- | native/src/window/icon.rs | 80 | 
11 files changed, 482 insertions, 154 deletions
diff --git a/native/Cargo.toml b/native/Cargo.toml index 3f92783e..1eedf0da 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -14,6 +14,7 @@ debug = []  twox-hash = { version = "1.5", default-features = false }  unicode-segmentation = "1.6"  num-traits = "0.2" +thiserror = "1"  [dependencies.iced_core]  version = "0.8" diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 16e78e82..0ff5e320 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -3,6 +3,8 @@ use crate::event::{self, Event};  use crate::window;  use crate::Hasher; +use iced_futures::futures::channel::mpsc; +use iced_futures::futures::never::Never;  use iced_futures::futures::{self, Future, Stream};  use iced_futures::{BoxStream, MaybeSend}; @@ -133,6 +135,27 @@ where  /// [`Stream`] that will call the provided closure to produce every `Message`.  ///  /// The `id` will be used to uniquely identify the [`Subscription`]. +pub fn unfold<I, T, Fut, Message>( +    id: I, +    initial: T, +    mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, +) -> Subscription<Message> +where +    I: Hash + 'static, +    T: MaybeSend + 'static, +    Fut: Future<Output = (Message, T)> + MaybeSend + 'static, +    Message: 'static + MaybeSend, +{ +    use futures::future::FutureExt; + +    run_with_id( +        id, +        futures::stream::unfold(initial, move |state| f(state).map(Some)), +    ) +} + +/// Creates a [`Subscription`] that publishes the events sent from a [`Future`] +/// to an [`mpsc::Sender`] with the given bounds.  ///  /// # Creating an asynchronous worker with bidirectional communication  /// You can leverage this helper to create a [`Subscription`] that spawns @@ -145,6 +168,7 @@ where  /// ```  /// use iced_native::subscription::{self, Subscription};  /// use iced_native::futures::channel::mpsc; +/// use iced_native::futures::sink::SinkExt;  ///  /// pub enum Event {  ///     Ready(mpsc::Sender<Input>), @@ -165,27 +189,35 @@ where  /// fn some_worker() -> Subscription<Event> {  ///     struct SomeWorker;  /// -///     subscription::unfold(std::any::TypeId::of::<SomeWorker>(), State::Starting, |state| async move { -///         match state { -///             State::Starting => { -///                 // Create channel -///                 let (sender, receiver) = mpsc::channel(100); +///     subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move { +///         let mut state = State::Starting;  /// -///                 (Some(Event::Ready(sender)), State::Ready(receiver)) -///             } -///             State::Ready(mut receiver) => { -///                 use iced_native::futures::StreamExt; +///         loop { +///             match &mut state { +///                 State::Starting => { +///                     // Create channel +///                     let (sender, receiver) = mpsc::channel(100); +/// +///                     // Send the sender back to the application +///                     output.send(Event::Ready(sender)).await; +/// +///                     // We are ready to receive messages +///                     state = State::Ready(receiver); +///                 } +///                 State::Ready(receiver) => { +///                     use iced_native::futures::StreamExt;  /// -///                 // Read next input sent from `Application` -///                 let input = receiver.select_next_some().await; +///                     // Read next input sent from `Application` +///                     let input = receiver.select_next_some().await;  /// -///                 match input { -///                     Input::DoSomeWork => { -///                         // Do some async work... +///                     match input { +///                         Input::DoSomeWork => { +///                             // Do some async work...  /// -///                         // Finally, we can optionally return a message to tell the -///                         // `Application` the work is done -///                         (Some(Event::WorkFinished), State::Ready(receiver)) +///                             // Finally, we can optionally produce a message to tell the +///                             // `Application` the work is done +///                             output.send(Event::WorkFinished).await; +///                         }  ///                     }  ///                 }  ///             } @@ -198,25 +230,28 @@ where  /// connection open.  ///  /// [`websocket`]: https://github.com/iced-rs/iced/tree/0.8/examples/websocket -pub fn unfold<I, T, Fut, Message>( +pub fn channel<I, Fut, Message>(      id: I, -    initial: T, -    mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, +    size: usize, +    f: impl Fn(mpsc::Sender<Message>) -> Fut + MaybeSend + Sync + 'static,  ) -> Subscription<Message>  where      I: Hash + 'static, -    T: MaybeSend + 'static, -    Fut: Future<Output = (Option<Message>, T)> + MaybeSend + 'static, +    Fut: Future<Output = Never> + MaybeSend + 'static,      Message: 'static + MaybeSend,  { -    use futures::future::{self, FutureExt}; -    use futures::stream::StreamExt; +    use futures::stream::{self, StreamExt}; -    run_with_id( +    Subscription::from_recipe(Runner {          id, -        futures::stream::unfold(initial, move |state| f(state).map(Some)) -            .filter_map(future::ready), -    ) +        spawn: move |_| { +            let (sender, receiver) = mpsc::channel(size); + +            let runner = stream::once(f(sender)).map(|_| unreachable!()); + +            stream::select(receiver, runner) +        }, +    })  }  struct Runner<I, F, S, Message> diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 9b69e574..ad05a8e7 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -14,17 +14,6 @@ use crate::{  pub use iced_style::checkbox::{Appearance, StyleSheet}; -/// The icon in a [`Checkbox`]. -#[derive(Debug, Clone, PartialEq)] -pub struct Icon<Font> { -    /// Font that will be used to display the `code_point`, -    pub font: Font, -    /// The unicode code point that will be used as the icon. -    pub code_point: char, -    /// Font size of the content. -    pub size: Option<f32>, -} -  /// A box that can be checked.  ///  /// # Example @@ -319,3 +308,14 @@ where          Element::new(checkbox)      }  } + +/// The icon in a [`Checkbox`]. +#[derive(Debug, Clone, PartialEq)] +pub struct Icon<Font> { +    /// Font that will be used to display the `code_point`, +    pub font: Font, +    /// The unicode code point that will be used as the icon. +    pub code_point: char, +    /// Font size of the content. +    pub size: Option<f32>, +} diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs index d13eca75..b25e064d 100644 --- a/native/src/widget/helpers.rs +++ b/native/src/widget/helpers.rs @@ -147,7 +147,7 @@ where      Renderer::Theme: widget::radio::StyleSheet,      V: Copy + Eq,  { -    widget::Radio::new(value, label, selected, on_click) +    widget::Radio::new(label, value, selected, on_click)  }  /// Creates a new [`Toggler`]. @@ -171,14 +171,13 @@ where  pub fn text_input<'a, Message, Renderer>(      placeholder: &str,      value: &str, -    on_change: impl Fn(String) -> Message + 'a,  ) -> widget::TextInput<'a, Message, Renderer>  where      Message: Clone,      Renderer: crate::text::Renderer,      Renderer::Theme: widget::text_input::StyleSheet,  { -    widget::TextInput::new(placeholder, value, on_change) +    widget::TextInput::new(placeholder, value)  }  /// Creates a new [`Slider`]. diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 9daddfbc..3ca041bf 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -21,10 +21,13 @@ pub use iced_style::radio::{Appearance, StyleSheet};  /// # type Radio<Message> =  /// #     iced_native::widget::Radio<Message, iced_native::renderer::Null>;  /// # +/// # use iced_native::column;  /// #[derive(Debug, Clone, Copy, PartialEq, Eq)]  /// pub enum Choice {  ///     A,  ///     B, +///     C, +///     All,  /// }  ///  /// #[derive(Debug, Clone, Copy)] @@ -34,12 +37,36 @@ pub use iced_style::radio::{Appearance, StyleSheet};  ///  /// let selected_choice = Some(Choice::A);  /// -/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected); +/// let a = Radio::new( +///     "A", +///     Choice::A, +///     selected_choice, +///     Message::RadioSelected, +/// );  /// -/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected); -/// ``` +/// let b = Radio::new( +///     "B", +///     Choice::B, +///     selected_choice, +///     Message::RadioSelected, +/// ); +/// +/// let c = Radio::new( +///     "C", +///     Choice::C, +///     selected_choice, +///     Message::RadioSelected, +/// ); +/// +/// let all = Radio::new( +///     "All of the above", +///     Choice::All, +///     selected_choice, +///     Message::RadioSelected +/// );  /// -///  +/// let content = column![a, b, c, all]; +/// ```  #[allow(missing_debug_implementations)]  pub struct Radio<Message, Renderer>  where @@ -78,8 +105,8 @@ where      ///   * a function that will be called when the [`Radio`] is selected. It      ///   receives the value of the radio and must produce a `Message`.      pub fn new<F, V>( -        value: V,          label: impl Into<String>, +        value: V,          selected: Option<V>,          f: F,      ) -> Self diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index d3715b1c..69c06140 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -8,13 +8,15 @@ use crate::renderer;  use crate::touch;  use crate::widget::tree::{self, Tree};  use crate::{ -    Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, -    Rectangle, Shell, Size, Widget, +    Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, +    Size, Widget,  };  use std::ops::RangeInclusive; -pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; +pub use iced_style::slider::{ +    Appearance, Handle, HandleShape, Rail, StyleSheet, +};  /// An horizontal bar and a handle that selects a single value from a range of  /// values. @@ -368,38 +370,6 @@ pub fn draw<T, R>(          style_sheet.active(style)      }; -    let rail_y = bounds.y + (bounds.height / 2.0).round(); - -    renderer.fill_quad( -        renderer::Quad { -            bounds: Rectangle { -                x: bounds.x, -                y: rail_y - 1.0, -                width: bounds.width, -                height: 2.0, -            }, -            border_radius: 0.0.into(), -            border_width: 0.0, -            border_color: Color::TRANSPARENT, -        }, -        style.rail_colors.0, -    ); - -    renderer.fill_quad( -        renderer::Quad { -            bounds: Rectangle { -                x: bounds.x, -                y: rail_y + 1.0, -                width: bounds.width, -                height: 2.0, -            }, -            border_radius: 0.0.into(), -            border_width: 0.0, -            border_color: Color::TRANSPARENT, -        }, -        Background::Color(style.rail_colors.1), -    ); -      let (handle_width, handle_height, handle_border_radius) = match style          .handle          .shape @@ -418,17 +388,61 @@ pub fn draw<T, R>(          (start.into() as f32, end.into() as f32)      }; -    let handle_offset = if range_start >= range_end { +    let offset = if range_start >= range_end {          0.0      } else {          (bounds.width - handle_width) * (value - range_start)              / (range_end - range_start)      }; +    let rail_y = bounds.y + bounds.height / 2.0; + +    renderer.fill_quad( +        renderer::Quad { +            bounds: Rectangle { +                x: bounds.x, +                y: rail_y - style.rail.width / 2.0, +                width: offset, +                height: style.rail.width, +            }, +            border_radius: [ +                style.rail.border_radius, +                0.0, +                0.0, +                style.rail.border_radius, +            ] +            .into(), +            border_width: 0.0, +            border_color: Color::TRANSPARENT, +        }, +        style.rail.colors.0, +    ); + +    renderer.fill_quad( +        renderer::Quad { +            bounds: Rectangle { +                x: bounds.x + offset, +                y: rail_y - style.rail.width / 2.0, +                width: bounds.width - offset, +                height: style.rail.width, +            }, +            border_radius: [ +                0.0, +                style.rail.border_radius, +                style.rail.border_radius, +                0.0, +            ] +            .into(), +            border_width: 0.0, +            border_color: Color::TRANSPARENT, +        }, +        style.rail.colors.1, +    ); +      renderer.fill_quad(          renderer::Quad {              bounds: Rectangle { -                x: bounds.x + handle_offset.round(), +                x: bounds.x + offset.round(),                  y: rail_y - handle_height / 2.0,                  width: handle_width,                  height: handle_height, diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index ee0473ea..8627aa98 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -46,8 +46,8 @@ pub use iced_style::text_input::{Appearance, StyleSheet};  /// let input = TextInput::new(  ///     "This is the placeholder...",  ///     value, -///     Message::TextInputChanged,  /// ) +/// .on_input(Message::TextInputChanged)  /// .padding(10);  /// ```  ///  @@ -65,9 +65,10 @@ where      width: Length,      padding: Padding,      size: Option<f32>, -    on_change: Box<dyn Fn(String) -> Message + 'a>, +    on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,      on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,      on_submit: Option<Message>, +    icon: Option<Icon<Renderer::Font>>,      style: <Renderer::Theme as StyleSheet>::Style,  } @@ -81,12 +82,8 @@ where      ///      /// It expects:      /// - a placeholder, -    /// - the current value, and -    /// - a function that produces a message when the [`TextInput`] changes. -    pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self -    where -        F: 'a + Fn(String) -> Message, -    { +    /// - the current value +    pub fn new(placeholder: &str, value: &str) -> Self {          TextInput {              id: None,              placeholder: String::from(placeholder), @@ -96,9 +93,10 @@ where              width: Length::Fill,              padding: Padding::new(5.0),              size: None, -            on_change: Box::new(on_change), +            on_input: None,              on_paste: None,              on_submit: None, +            icon: None,              style: Default::default(),          }      } @@ -115,6 +113,25 @@ where          self      } +    /// Sets the message that should be produced when some text is typed into +    /// the [`TextInput`]. +    /// +    /// If this method is not called, the [`TextInput`] will be disabled. +    pub fn on_input<F>(mut self, callback: F) -> Self +    where +        F: 'a + Fn(String) -> Message, +    { +        self.on_input = Some(Box::new(callback)); +        self +    } + +    /// Sets the message that should be produced when the [`TextInput`] is +    /// focused and the enter key is pressed. +    pub fn on_submit(mut self, message: Message) -> Self { +        self.on_submit = Some(message); +        self +    } +      /// Sets the message that should be produced when some text is pasted into      /// the [`TextInput`].      pub fn on_paste( @@ -132,6 +149,13 @@ where          self.font = font;          self      } + +    /// Sets the [`Icon`] of the [`TextInput`]. +    pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self { +        self.icon = Some(icon); +        self +    } +      /// Sets the width of the [`TextInput`].      pub fn width(mut self, width: impl Into<Length>) -> Self {          self.width = width.into(); @@ -150,13 +174,6 @@ where          self      } -    /// Sets the message that should be produced when the [`TextInput`] is -    /// focused and the enter key is pressed. -    pub fn on_submit(mut self, message: Message) -> Self { -        self.on_submit = Some(message); -        self -    } -      /// Sets the style of the [`TextInput`].      pub fn style(          mut self, @@ -189,7 +206,9 @@ where              &self.placeholder,              self.size,              &self.font, +            self.on_input.is_none(),              self.is_secure, +            self.icon.as_ref(),              &self.style,          )      } @@ -210,6 +229,18 @@ where          tree::State::new(State::new())      } +    fn diff(&self, tree: &mut Tree) { +        let state = tree.state.downcast_mut::<State>(); + +        // Unfocus text input if it becomes disabled +        if self.on_input.is_none() { +            state.last_click = None; +            state.is_focused = None; +            state.is_pasting = None; +            state.is_dragging = false; +        } +    } +      fn width(&self) -> Length {          self.width      } @@ -223,7 +254,14 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        layout(renderer, limits, self.width, self.padding, self.size) +        layout( +            renderer, +            limits, +            self.width, +            self.padding, +            self.size, +            self.icon.as_ref(), +        )      }      fn operate( @@ -260,7 +298,7 @@ where              self.size,              &self.font,              self.is_secure, -            self.on_change.as_ref(), +            self.on_input.as_deref(),              self.on_paste.as_deref(),              &self.on_submit,              || tree.state.downcast_mut::<State>(), @@ -287,7 +325,9 @@ where              &self.placeholder,              self.size,              &self.font, +            self.on_input.is_none(),              self.is_secure, +            self.icon.as_ref(),              &self.style,          )      } @@ -300,7 +340,7 @@ where          _viewport: &Rectangle,          _renderer: &Renderer,      ) -> mouse::Interaction { -        mouse_interaction(layout, cursor_position) +        mouse_interaction(layout, cursor_position, self.on_input.is_none())      }  } @@ -318,6 +358,30 @@ where      }  } +/// The content of the [`Icon`]. +#[derive(Debug, Clone)] +pub struct Icon<Font> { +    /// The font that will be used to display the `code_point`. +    pub font: Font, +    /// The unicode code point that will be used as the icon. +    pub code_point: char, +    /// The font size of the content. +    pub size: Option<f32>, +    /// The spacing between the [`Icon`] and the text in a [`TextInput`]. +    pub spacing: f32, +    /// The side of a [`TextInput`] where to display the [`Icon`]. +    pub side: Side, +} + +/// The side of a [`TextInput`]. +#[derive(Debug, Clone)] +pub enum Side { +    /// The left side of a [`TextInput`]. +    Left, +    /// The right side of a [`TextInput`]. +    Right, +} +  /// The identifier of a [`TextInput`].  #[derive(Debug, Clone, PartialEq, Eq, Hash)]  pub struct Id(widget::Id); @@ -380,6 +444,7 @@ pub fn layout<Renderer>(      width: Length,      padding: Padding,      size: Option<f32>, +    icon: Option<&Icon<Renderer::Font>>,  ) -> layout::Node  where      Renderer: text::Renderer, @@ -389,10 +454,51 @@ where      let padding = padding.fit(Size::ZERO, limits.max());      let limits = limits.width(width).pad(padding).height(text_size); -    let mut text = layout::Node::new(limits.resolve(Size::ZERO)); -    text.move_to(Point::new(padding.left, padding.top)); +    let text_bounds = limits.resolve(Size::ZERO); + +    if let Some(icon) = icon { +        let icon_width = renderer.measure_width( +            &icon.code_point.to_string(), +            icon.size.unwrap_or_else(|| renderer.default_size()), +            icon.font.clone(), +        ); + +        let mut text_node = layout::Node::new( +            text_bounds - Size::new(icon_width + icon.spacing, 0.0), +        ); + +        let mut icon_node = +            layout::Node::new(Size::new(icon_width, text_bounds.height)); + +        match icon.side { +            Side::Left => { +                text_node.move_to(Point::new( +                    padding.left + icon_width + icon.spacing, +                    padding.top, +                )); + +                icon_node.move_to(Point::new(padding.left, padding.top)); +            } +            Side::Right => { +                text_node.move_to(Point::new(padding.left, padding.top)); + +                icon_node.move_to(Point::new( +                    padding.left + text_bounds.width - icon_width, +                    padding.top, +                )); +            } +        }; + +        layout::Node::with_children( +            text_bounds.pad(padding), +            vec![text_node, icon_node], +        ) +    } else { +        let mut text = layout::Node::new(text_bounds); +        text.move_to(Point::new(padding.left, padding.top)); -    layout::Node::with_children(text.size().pad(padding), vec![text]) +        layout::Node::with_children(text_bounds.pad(padding), vec![text]) +    }  }  /// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] @@ -408,7 +514,7 @@ pub fn update<'a, Message, Renderer>(      size: Option<f32>,      font: &Renderer::Font,      is_secure: bool, -    on_change: &dyn Fn(String) -> Message, +    on_input: Option<&dyn Fn(String) -> Message>,      on_paste: Option<&dyn Fn(String) -> Message>,      on_submit: &Option<Message>,      state: impl FnOnce() -> &'a mut State, @@ -421,7 +527,8 @@ where          Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))          | Event::Touch(touch::Event::FingerPressed { .. }) => {              let state = state(); -            let is_clicked = layout.bounds().contains(cursor_position); +            let is_clicked = +                layout.bounds().contains(cursor_position) && on_input.is_some();              state.is_focused = if is_clicked {                  state.is_focused.or_else(|| { @@ -551,6 +658,8 @@ where              let state = state();              if let Some(focus) = &mut state.is_focused { +                let Some(on_input) = on_input else { return event::Status::Ignored }; +                  if state.is_pasting.is_none()                      && !state.keyboard_modifiers.command()                      && !c.is_control() @@ -559,7 +668,7 @@ where                      editor.insert(c); -                    let message = (on_change)(editor.contents()); +                    let message = (on_input)(editor.contents());                      shell.publish(message);                      focus.updated_at = Instant::now(); @@ -572,6 +681,8 @@ where              let state = state();              if let Some(focus) = &mut state.is_focused { +                let Some(on_input) = on_input else { return event::Status::Ignored }; +                  let modifiers = state.keyboard_modifiers;                  focus.updated_at = Instant::now(); @@ -597,7 +708,7 @@ where                          let mut editor = Editor::new(value, &mut state.cursor);                          editor.backspace(); -                        let message = (on_change)(editor.contents()); +                        let message = (on_input)(editor.contents());                          shell.publish(message);                      }                      keyboard::KeyCode::Delete => { @@ -617,7 +728,7 @@ where                          let mut editor = Editor::new(value, &mut state.cursor);                          editor.delete(); -                        let message = (on_change)(editor.contents()); +                        let message = (on_input)(editor.contents());                          shell.publish(message);                      }                      keyboard::KeyCode::Left => { @@ -692,7 +803,7 @@ where                          let mut editor = Editor::new(value, &mut state.cursor);                          editor.delete(); -                        let message = (on_change)(editor.contents()); +                        let message = (on_input)(editor.contents());                          shell.publish(message);                      }                      keyboard::KeyCode::V => { @@ -719,7 +830,7 @@ where                              let message = if let Some(paste) = &on_paste {                                  (paste)(editor.contents())                              } else { -                                (on_change)(editor.contents()) +                                (on_input)(editor.contents())                              };                              shell.publish(message); @@ -813,7 +924,9 @@ pub fn draw<Renderer>(      placeholder: &str,      size: Option<f32>,      font: &Renderer::Font, +    is_disabled: bool,      is_secure: bool, +    icon: Option<&Icon<Renderer::Font>>,      style: &<Renderer::Theme as StyleSheet>::Style,  ) where      Renderer: text::Renderer, @@ -823,11 +936,15 @@ pub fn draw<Renderer>(      let value = secure_value.as_ref().unwrap_or(value);      let bounds = layout.bounds(); -    let text_bounds = layout.children().next().unwrap().bounds(); + +    let mut children_layout = layout.children(); +    let text_bounds = children_layout.next().unwrap().bounds();      let is_mouse_over = bounds.contains(cursor_position); -    let appearance = if state.is_focused() { +    let appearance = if is_disabled { +        theme.disabled(style) +    } else if state.is_focused() {          theme.focused(style)      } else if is_mouse_over {          theme.hovered(style) @@ -845,6 +962,20 @@ pub fn draw<Renderer>(          appearance.background,      ); +    if let Some(icon) = icon { +        let icon_layout = children_layout.next().unwrap(); + +        renderer.fill_text(Text { +            content: &icon.code_point.to_string(), +            size: icon.size.unwrap_or_else(|| renderer.default_size()), +            font: icon.font.clone(), +            color: appearance.icon_color, +            bounds: icon_layout.bounds(), +            horizontal_alignment: alignment::Horizontal::Left, +            vertical_alignment: alignment::Vertical::Top, +        }); +    } +      let text = value.to_string();      let size = size.unwrap_or_else(|| renderer.default_size()); @@ -956,6 +1087,8 @@ pub fn draw<Renderer>(              content: if text.is_empty() { placeholder } else { &text },              color: if text.is_empty() {                  theme.placeholder_color(style) +            } else if is_disabled { +                theme.disabled_color(style)              } else {                  theme.value_color(style)              }, @@ -984,9 +1117,14 @@ pub fn draw<Renderer>(  pub fn mouse_interaction(      layout: Layout<'_>,      cursor_position: Point, +    is_disabled: bool,  ) -> mouse::Interaction {      if layout.bounds().contains(cursor_position) { -        mouse::Interaction::Text +        if is_disabled { +            mouse::Interaction::NotAllowed +        } else { +            mouse::Interaction::Text +        }      } else {          mouse::Interaction::default()      } diff --git a/native/src/widget/vertical_slider.rs b/native/src/widget/vertical_slider.rs index f1687e38..a06a200f 100644 --- a/native/src/widget/vertical_slider.rs +++ b/native/src/widget/vertical_slider.rs @@ -8,8 +8,8 @@ pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};  use crate::event::{self, Event};  use crate::widget::tree::{self, Tree};  use crate::{ -    layout, mouse, renderer, touch, Background, Clipboard, Color, Element, -    Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget, +    layout, mouse, renderer, touch, Clipboard, Color, Element, Layout, Length, +    Pixels, Point, Rectangle, Shell, Size, Widget,  };  /// An vertical bar and a handle that selects a single value from a range of @@ -363,38 +363,6 @@ pub fn draw<T, R>(          style_sheet.active(style)      }; -    let rail_x = bounds.x + (bounds.width / 2.0).round(); - -    renderer.fill_quad( -        renderer::Quad { -            bounds: Rectangle { -                x: rail_x - 1.0, -                y: bounds.y, -                width: 2.0, -                height: bounds.height, -            }, -            border_radius: 0.0.into(), -            border_width: 0.0, -            border_color: Color::TRANSPARENT, -        }, -        style.rail_colors.0, -    ); - -    renderer.fill_quad( -        renderer::Quad { -            bounds: Rectangle { -                x: rail_x + 1.0, -                y: bounds.y, -                width: 2.0, -                height: bounds.height, -            }, -            border_radius: 0.0.into(), -            border_width: 0.0, -            border_color: Color::TRANSPARENT, -        }, -        Background::Color(style.rail_colors.1), -    ); -      let (handle_width, handle_height, handle_border_radius) = match style          .handle          .shape @@ -413,18 +381,62 @@ pub fn draw<T, R>(          (start.into() as f32, end.into() as f32)      }; -    let handle_offset = if range_start >= range_end { +    let offset = if range_start >= range_end {          0.0      } else {          (bounds.height - handle_width) * (value - range_end)              / (range_start - range_end)      }; +    let rail_x = bounds.x + bounds.width / 2.0; + +    renderer.fill_quad( +        renderer::Quad { +            bounds: Rectangle { +                x: rail_x - style.rail.width / 2.0, +                y: bounds.y, +                width: style.rail.width, +                height: offset, +            }, +            border_radius: [ +                style.rail.border_radius, +                style.rail.border_radius, +                0.0, +                0.0, +            ] +            .into(), +            border_width: 0.0, +            border_color: Color::TRANSPARENT, +        }, +        style.rail.colors.1, +    ); + +    renderer.fill_quad( +        renderer::Quad { +            bounds: Rectangle { +                x: rail_x - style.rail.width / 2.0, +                y: bounds.y + offset, +                width: style.rail.width, +                height: bounds.height - offset, +            }, +            border_radius: [ +                0.0, +                0.0, +                style.rail.border_radius, +                style.rail.border_radius, +            ] +            .into(), +            border_width: 0.0, +            border_color: Color::TRANSPARENT, +        }, +        style.rail.colors.0, +    ); +      renderer.fill_quad(          renderer::Quad {              bounds: Rectangle { -                x: rail_x - (handle_height / 2.0), -                y: bounds.y + handle_offset.round(), +                x: rail_x - handle_height / 2.0, +                y: bounds.y + offset.round(),                  width: handle_height,                  height: handle_width,              }, diff --git a/native/src/window.rs b/native/src/window.rs index a5cdc8ce..1ae89dba 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -5,8 +5,11 @@ mod mode;  mod redraw_request;  mod user_attention; +pub mod icon; +  pub use action::Action;  pub use event::Event; +pub use icon::Icon;  pub use mode::Mode;  pub use redraw_request::RedrawRequest;  pub use user_attention::UserAttention; diff --git a/native/src/window/action.rs b/native/src/window/action.rs index ce36d129..095a8eec 100644 --- a/native/src/window/action.rs +++ b/native/src/window/action.rs @@ -1,4 +1,4 @@ -use crate::window::{Mode, UserAttention}; +use crate::window::{Icon, Mode, UserAttention};  use iced_futures::MaybeSend;  use std::fmt; @@ -78,6 +78,21 @@ pub enum Action<T> {      ChangeAlwaysOnTop(bool),      /// Fetch an identifier unique to the window.      FetchId(Box<dyn FnOnce(u64) -> T + 'static>), +    /// Changes the window [`Icon`]. +    /// +    /// On Windows and X11, this is typically the small icon in the top-left +    /// corner of the titlebar. +    /// +    /// ## Platform-specific +    /// +    /// - **Web / Wayland / macOS:** Unsupported. +    /// +    /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's +    ///   recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. +    /// +    /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That +    ///   said, it's usually in the same ballpark as on Windows. +    ChangeIcon(Icon),  }  impl<T> Action<T> { @@ -108,6 +123,7 @@ impl<T> Action<T> {                  Action::ChangeAlwaysOnTop(on_top)              }              Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), +            Self::ChangeIcon(icon) => Action::ChangeIcon(icon),          }      }  } @@ -142,6 +158,9 @@ impl<T> fmt::Debug for Action<T> {                  write!(f, "Action::AlwaysOnTop({on_top})")              }              Self::FetchId(_) => write!(f, "Action::FetchId"), +            Self::ChangeIcon(_icon) => { +                write!(f, "Action::ChangeIcon(icon)") +            }          }      }  } diff --git a/native/src/window/icon.rs b/native/src/window/icon.rs new file mode 100644 index 00000000..31868ecf --- /dev/null +++ b/native/src/window/icon.rs @@ -0,0 +1,80 @@ +//! Change the icon of a window. +use crate::Size; + +use std::mem; + +/// Builds an  [`Icon`] from its RGBA pixels in the sRGB color space. +pub fn from_rgba( +    rgba: Vec<u8>, +    width: u32, +    height: u32, +) -> Result<Icon, Error> { +    const PIXEL_SIZE: usize = mem::size_of::<u8>() * 4; + +    if rgba.len() % PIXEL_SIZE != 0 { +        return Err(Error::ByteCountNotDivisibleBy4 { +            byte_count: rgba.len(), +        }); +    } + +    let pixel_count = rgba.len() / PIXEL_SIZE; + +    if pixel_count != (width * height) as usize { +        return Err(Error::DimensionsVsPixelCount { +            width, +            height, +            width_x_height: (width * height) as usize, +            pixel_count, +        }); +    } + +    Ok(Icon { +        rgba, +        size: Size::new(width, height), +    }) +} + +/// An window icon normally used for the titlebar or taskbar. +#[derive(Debug, Clone)] +pub struct Icon { +    rgba: Vec<u8>, +    size: Size<u32>, +} + +impl Icon { +    /// Returns the raw data of the [`Icon`]. +    pub fn into_raw(self) -> (Vec<u8>, Size<u32>) { +        (self.rgba, self.size) +    } +} + +#[derive(Debug, thiserror::Error)] +/// An error produced when using [`Icon::from_rgba`] with invalid arguments. +pub enum Error { +    /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be +    /// safely interpreted as 32bpp RGBA pixels. +    #[error( +        "The provided RGBA data (with length {byte_count}) isn't divisible \ +        by 4. Therefore, it cannot be safely interpreted as 32bpp RGBA pixels" +    )] +    ByteCountNotDivisibleBy4 { +        /// The length of the provided RGBA data. +        byte_count: usize, +    }, +    /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. +    /// At least one of your arguments is incorrect. +    #[error( +        "The number of RGBA pixels ({pixel_count}) does not match the \ +        provided dimensions ({width}x{height})." +    )] +    DimensionsVsPixelCount { +        /// The provided width. +        width: u32, +        /// The provided height. +        height: u32, +        /// The product of `width` and `height`. +        width_x_height: usize, +        /// The amount of pixels of the provided RGBA data. +        pixel_count: usize, +    }, +}  | 
