Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/rotation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "rotation"
version = "0.1.0"
edition = "2021"

[dependencies]
im.workspace = true
floem = { path = "../.." }
163 changes: 163 additions & 0 deletions examples/rotation/src/main.rs
Original file line number Diff line number Diff line change
@@ -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);
}
40 changes: 40 additions & 0 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ impl StylePropValue for cosmic_text::Style {}
impl StylePropValue for TextOverflow {}
impl StylePropValue for LineHeightValue {}
impl StylePropValue for Size<LengthPercentage> {}
impl StylePropValue for Rotation {}

impl<T: StylePropValue> StylePropValue for Option<T> {
fn debug_view(&self) -> Option<AnyView> {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1117,6 +1126,16 @@ impl<T> From<T> for StyleValue<T> {
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 {
(
Expand Down Expand Up @@ -1213,6 +1232,7 @@ define_builtin_props!(
LineHeight line_height nocb: Option<LineHeightValue> { inherited } = None,
AspectRatio aspect_ratio: Option<f32> {} = None,
Gap gap nocb: Size<LengthPercentage> {} = Size::zero(),
RotationProp rotation: Rotation {} = Rotation::NoRotation,
);

prop_extractor! {
Expand Down Expand Up @@ -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
Expand Down
39 changes: 35 additions & 4 deletions src/views/label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ 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! {
Extracter {
color: TextColor,
text_overflow: TextOverflowProp,
line_height: LineHeight,
rotation: RotationProp,
}
}

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
}
}
16 changes: 12 additions & 4 deletions vger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,12 +404,19 @@ impl Renderer for VgerRenderer {
fn draw_text(&mut self, layout: &TextLayout, pos: impl Into<Point>) {
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;
}
Expand All @@ -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 {
Expand Down Expand Up @@ -463,6 +470,7 @@ impl Renderer for VgerRenderer {
}
}
}
self.vger.rotate(-theta);
}

fn draw_img(&mut self, img: Img<'_>, rect: Rect) {
Expand Down