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, + }, +} |