diff --git a/examples/rotation/Cargo.toml b/examples/rotation/Cargo.toml new file mode 100644 index 000000000..ffb9cde11 --- /dev/null +++ b/examples/rotation/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rotation" +version = "0.1.0" +edition = "2021" + +[dependencies] +im.workspace = true +floem = { path = "../.." } diff --git a/examples/rotation/src/main.rs b/examples/rotation/src/main.rs new file mode 100644 index 000000000..91d8ca8a2 --- /dev/null +++ b/examples/rotation/src/main.rs @@ -0,0 +1,163 @@ +use floem::{ + event::{Event, EventListener}, + keyboard::{Key, NamedKey}, + peniko::Color, + reactive::create_signal, + style::{Background, BorderColor, Outline, OutlineColor, Style, TextColor, Transition}, + style_class, + view::View, + views::{label, stack, text, Decorators}, +}; + +style_class!(pub Button); +style_class!(pub Label); +style_class!(pub Frame); + +fn app_view() -> impl View { + let blue_button = Style::new() + .background(Color::rgb8(137, 145, 160)) + .color(Color::WHITE) + .border(1.0) + .border_color(Color::rgb8(109, 121, 135)) + .hover(|s| s.background(Color::rgb8(170, 175, 187))) + .transition(TextColor, Transition::linear(0.06)) + .transition(BorderColor, Transition::linear(0.06)) + .transition(Background, Transition::linear(0.06)) + .transition(Outline, Transition::linear(0.1)) + .focus_visible(|s| { + s.outline(2.0) + .outline_color(Color::WHITE.with_alpha_factor(0.7)) + }) + .disabled(|s| { + s.background(Color::DARK_GRAY.with_alpha_factor(0.1)) + .border_color(Color::BLACK.with_alpha_factor(0.2)) + }) + .active(|s| s.background(Color::BLACK.with_alpha_factor(0.4))) + .padding(5.0) + .margin(3.0) + .border_radius(5.0); + let blue_theme = Style::new() + .background(Color::rgb8(95, 102, 118)) + .transition(Background, Transition::linear(0.1)) + .transition(TextColor, Transition::linear(0.1)) + .color(Color::WHITE) + .class(Button, move |_| blue_button) + .class(Label, |s| { + s.margin(4.0).transition(TextColor, Transition::linear(0.1)) + }) + .font_size(12.0); + + let green_button = Style::new() + .background(Color::rgb8(180, 188, 175)) + .disabled(|s| { + s.background(Color::rgb8(180, 188, 175).with_alpha_factor(0.3)) + .border_color(Color::rgb8(131, 145, 123).with_alpha_factor(0.3)) + .color(Color::GRAY) + }) + .active(|s| s.background(Color::rgb8(95, 105, 88)).color(Color::WHITE)) + .color(Color::BLACK.with_alpha_factor(0.7)) + .border(2.0) + .transition(TextColor, Transition::linear(0.3)) + .transition(BorderColor, Transition::linear(0.3)) + .transition(Background, Transition::linear(0.3)) + .transition(Outline, Transition::linear(0.2)) + .transition(OutlineColor, Transition::linear(0.2)) + .outline_color(Color::rgba8(131, 145, 123, 0)) + .focus_visible(|s| { + s.outline(10.0) + .outline_color(Color::rgb8(131, 145, 123).with_alpha_factor(0.3)) + }) + .border_color(Color::rgb8(131, 145, 123)) + .hover(|s| s.background(Color::rgb8(204, 209, 201))) + .padding(8.0) + .border_radius(8.0) + .margin(6.0); + let green_theme = Style::new() + .background(Color::rgb8(227, 231, 226)) + .transition(Background, Transition::linear(0.5)) + .class(Button, move |_| green_button) + .class(Label, |s| { + s.margin(4.0).transition(TextColor, Transition::linear(0.5)) + }) + .class(Frame, |s| { + s.border(2.0) + .border_color(Color::rgb8(131, 145, 123).with_alpha_factor(0.2)) + .border_radius(8.0) + .background(Color::WHITE.with_alpha_factor(0.1)) + .padding(12.0) + }) + .color(Color::BLACK.with_alpha_factor(0.5)) + .font_size(16.0); + + let (counter, set_counter) = create_signal(0); + let (theme, set_theme) = create_signal(false); + let view = stack((stack(( + text("Toggle Theme") + .class(Button) + .on_click_stop({ + move |_| { + set_theme.update(|theme| *theme = !*theme); + } + }) + .style(|s| s.rotate_right()) + .keyboard_navigatable(), + stack(( + label(move || format!("Value: {}", counter.get())).class(Label), + text("Increment") + .class(Button) + .on_click_stop({ + move |_| { + set_counter.update(|value| *value += 1); + } + }) + .style(|s| s.rotate_right()) + .keyboard_navigatable(), + text("Decrement") + .class(Button) + .on_click_stop({ + move |_| { + set_counter.update(|value| *value -= 1); + } + }) + .style(|s| s.rotate_invert()) + .keyboard_navigatable(), + text("Reset to 0") + .class(Button) + .on_click_stop(move |_| { + println!("Reset counter pressed"); // will not fire if button is disabled + set_counter.update(|value| *value = 0); + }) + .disabled(move || counter.get() == 0) + .style(|s| s.rotate_left()) + .keyboard_navigatable(), + )) + .class(Frame) + .style(|s| s.items_center()), + )) + .style(|s| s.items_center()),)) + .style(move |_| { + if theme.get() { + blue_theme.clone() + } else { + green_theme.clone() + } + .width_full() + .height_full() + .flex_col() + .items_center() + .justify_center() + }) + .window_title(|| "Themes Example".to_string()); + let id = view.id(); + view.on_event_stop(EventListener::KeyUp, move |e| { + if let Event::KeyUp(e) = e { + if e.key.logical_key == Key::Named(NamedKey::F11) { + id.inspect(); + } + } + }) +} + +fn main() { + floem::launch(app_view); +} diff --git a/src/style.rs b/src/style.rs index 4b0de7d77..d60be8fe6 100644 --- a/src/style.rs +++ b/src/style.rs @@ -72,6 +72,7 @@ impl StylePropValue for cosmic_text::Style {} impl StylePropValue for TextOverflow {} impl StylePropValue for LineHeightValue {} impl StylePropValue for Size {} +impl StylePropValue for Rotation {} impl StylePropValue for Option { fn debug_view(&self) -> Option { @@ -1020,6 +1021,14 @@ pub enum TextOverflow { Ellipsis, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Rotation { + NoRotation, + RotateRight, + RotateUpsideDown, + RotateLeft, +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum CursorStyle { Default, @@ -1117,6 +1126,16 @@ impl From for StyleValue { Self::Val(x) } } +impl Rotation { + pub fn angle(self) -> f64 { + match self { + Rotation::NoRotation => 0.0, + Rotation::RotateRight => 90.0, + Rotation::RotateUpsideDown => 180.0, + Rotation::RotateLeft => 270.0, + } + } +} macro_rules! define_builtin_props { ( @@ -1213,6 +1232,7 @@ define_builtin_props!( LineHeight line_height nocb: Option { inherited } = None, AspectRatio aspect_ratio: Option {} = None, Gap gap nocb: Size {} = Size::zero(), + RotationProp rotation: Rotation {} = Rotation::NoRotation, ); prop_extractor! { @@ -1716,6 +1736,26 @@ impl Style { self.set(ZIndex, Some(z_index)) } + ///rotation 0 + pub fn no_rotation(self) -> Self { + self.rotation(Rotation::NoRotation) + } + + ///rotation 90 + pub fn rotate_right(self) -> Self { + self.rotation(Rotation::RotateRight) + } + + ///rotation 180 + pub fn rotate_invert(self) -> Self { + self.rotation(Rotation::RotateUpsideDown) + } + + ///rotation 270 + pub fn rotate_left(self) -> Self { + self.rotation(Rotation::RotateLeft) + } + /// Allow the application of a function if the option exists. /// This is useful for chaining together a bunch of optional style changes. /// ```rust diff --git a/src/views/label.rs b/src/views/label.rs index 84479da7d..afd02fcc7 100644 --- a/src/views/label.rs +++ b/src/views/label.rs @@ -5,15 +5,15 @@ use crate::{ cosmic_text::{Attrs, AttrsList, FamilyOwned, TextLayout}, id::Id, prop_extractor, - style::Style, style::{FontProps, LineHeight, TextColor, TextOverflow, TextOverflowProp}, + style::{Rotation, RotationProp, Style}, unit::PxPct, view::{View, ViewData, Widget}, }; use floem_peniko::Color; use floem_reactive::create_updater; use floem_renderer::Renderer; -use kurbo::{Point, Rect}; +use kurbo::{Affine, Point, Rect}; use taffy::tree::NodeId; prop_extractor! { @@ -21,6 +21,7 @@ prop_extractor! { color: TextColor, text_overflow: TextOverflowProp, line_height: LineHeight, + rotation: RotationProp, } } @@ -226,6 +227,13 @@ impl Widget for Label { } let text_node = self.text_node.unwrap(); + let (width, height) = match self.style.rotation() { + Rotation::NoRotation => (width, height), + Rotation::RotateRight => (height, width), + Rotation::RotateUpsideDown => (width, height), + Rotation::RotateLeft => (height, width), + }; + let style = Style::new().width(width).height(height).to_taffy_style(); let _ = cx.app_state_mut().taffy.set_style(text_node, style); @@ -283,7 +291,13 @@ impl Widget for Label { if width > available_width { if self.available_width != Some(available_width) { let mut text_layout = text_layout.clone(); - text_layout.set_size(available_width, f32::MAX); + let (width, height) = match self.style.rotation() { + Rotation::NoRotation => (available_width, f32::MAX), + Rotation::RotateRight => (f32::MAX, available_width), + Rotation::RotateUpsideDown => (available_width, f32::MAX), + Rotation::RotateLeft => (f32::MAX, available_width), + }; + text_layout.set_size(width, height); self.available_text_layout = Some(text_layout); self.available_width = Some(available_width); cx.app_state_mut().request_layout(self.id()); @@ -315,13 +329,30 @@ impl Widget for Label { return; } + let text_layout = self.text_layout.as_ref().unwrap(); + let higth = text_layout.size().height as f32; + let width = text_layout.size().width as f32; let text_node = self.text_node.unwrap(); let location = cx.app_state.taffy.layout(text_node).unwrap().location; - let point = Point::new(location.x as f64, location.y as f64); + + let (x, y) = match self.style.rotation() { + Rotation::NoRotation => (location.x, location.y), + Rotation::RotateRight => ((location.x + higth), location.y), + Rotation::RotateUpsideDown => ((location.x + width), (location.y + higth)), + Rotation::RotateLeft => (location.x, (location.y + width)), + }; + let point = Point::new(x as f64, y as f64); + + let mut affine = cx.transform.as_coeffs(); + affine[0] = self.style.rotation().angle(); + cx.paint_state.renderer.transform(Affine::new(affine)); + if let Some(text_layout) = self.available_text_layout.as_ref() { cx.draw_text(text_layout, point); } else { cx.draw_text(self.text_layout.as_ref().unwrap(), point); } + + cx.paint_state.renderer.transform(cx.transform); } } diff --git a/vger/src/lib.rs b/vger/src/lib.rs index 28b9d4311..4c4aca3e2 100644 --- a/vger/src/lib.rs +++ b/vger/src/lib.rs @@ -404,12 +404,19 @@ impl Renderer for VgerRenderer { fn draw_text(&mut self, layout: &TextLayout, pos: impl Into) { let mut swash_cache = SwashCache::new(); let transform = self.transform.as_coeffs(); - let offset = Vec2::new(transform[4], transform[5]); + let pos: Point = pos.into(); + let pos = Point::new(pos.x + transform[4], pos.y + transform[5]); + // transform[0] : Angle + let theta = std::f32::consts::TAU * transform[0] as f32 / 360.0; + let pos = Affine::rotate(-theta as f64) * pos; + // Absolute coordinate rotation + self.vger.rotate(theta); + let clip = self.clip; for line in layout.layout_runs() { if let Some(rect) = clip { - let y = pos.y + offset.y + line.line_y as f64; + let y = pos.y + line.line_y as f64; if y + (line.line_height as f64) < rect.y0 { continue; } @@ -418,8 +425,8 @@ impl Renderer for VgerRenderer { } } 'line_loop: for glyph_run in line.glyphs { - let x = glyph_run.x + pos.x as f32 + offset.x as f32; - let y = line.line_y + pos.y as f32 + offset.y as f32; + let x = glyph_run.x + pos.x as f32; + let y = line.line_y + pos.y as f32; if let Some(rect) = clip { if ((x + glyph_run.w) as f64) < rect.x0 { @@ -463,6 +470,7 @@ impl Renderer for VgerRenderer { } } } + self.vger.rotate(-theta); } fn draw_img(&mut self, img: Img<'_>, rect: Rect) {