diff --git a/demo/src/window/group/group.blp b/demo/src/window/group/group.blp new file mode 100644 index 0000000..cfe9fdd --- /dev/null +++ b/demo/src/window/group/group.blp @@ -0,0 +1,636 @@ +using Gtk 4.0; +using Adw 1; + +template $OriDemoGroupPage: Adw.Bin { + child: Adw.StatusPage { + title: "Group"; + description: "Layouts widgets dependend on their aspect ratio"; + + child: Adw.Clamp { + maximum-size: 600; + tightening-threshold: 1; + + child: Box { + orientation: vertical; + + Adw.SpinRow { + styles [ + "card" + ] + + title: "Spacing"; + + adjustment: Adjustment spacing_adjustment { + lower: 0; + upper: 16; + step-increment: 1; + }; + } + + Box { + can-target: false; + orientation: horizontal; + hexpand: true; + spacing: 16; + + Box { + orientation: vertical; + hexpand: true; + spacing: 16; + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 5; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 2; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 3; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 9; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 9; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 9; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 5; + } + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 5; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 9; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + } + + Box { + orientation: vertical; + hexpand: true; + spacing: 16; + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 5; + } + + Button { + styles [ + "opaque" + ] + + width-request: 3; + height-request: 7; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 5; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 9; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 3; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 9; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 5; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 9; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 3; + height-request: 8; + } + } + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 7; + height-request: 8; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + + Box { + orientation: vertical; + + $OriGroup { + spacing: bind spacing_adjustment.value; + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 5; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 10; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 7; + } + + Button { + styles [ + "opaque" + ] + + width-request: 8; + height-request: 5; + } + + Button { + styles [ + "opaque" + ] + + width-request: 6; + height-request: 8; + } + } + } + } + } + }; + }; + }; +} diff --git a/demo/src/window/group/mod.rs b/demo/src/window/group/mod.rs new file mode 100644 index 0000000..a169baf --- /dev/null +++ b/demo/src/window/group/mod.rs @@ -0,0 +1,34 @@ +use adw::subclass::prelude::*; +use gtk::glib; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "src/window/group/group.blp")] + pub struct GroupPage; + + #[glib::object_subclass] + impl ObjectSubclass for GroupPage { + const NAME: &'static str = "OriDemoGroupPage"; + type Type = super::GroupPage; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for GroupPage {} + impl WidgetImpl for GroupPage {} + impl BinImpl for GroupPage {} +} + +glib::wrapper! { + pub struct GroupPage(ObjectSubclass) + @extends gtk::Widget; +} diff --git a/demo/src/window/mod.rs b/demo/src/window/mod.rs index 896c895..2afe976 100644 --- a/demo/src/window/mod.rs +++ b/demo/src/window/mod.rs @@ -1,4 +1,6 @@ +mod group; mod loading_indicator; +mod picture_group; mod shimmer_effect; mod spoiler; @@ -13,8 +15,14 @@ mod imp { #[derive(Debug, Default, gtk::CompositeTemplate)] #[template(file = "src/window/window.blp")] pub struct SpoilerWindow { + #[template_child] + pub(super) leaflet: TemplateChild, + #[template_child] + pub(super) sidebar: TemplateChild, #[template_child] pub(super) stack: TemplateChild, + #[template_child] + pub(super) content: TemplateChild, } #[glib::object_subclass] @@ -27,8 +35,11 @@ mod imp { loading_indicator::LoadingIndicatorPage::static_type(); shimmer_effect::ShimmerEffectPage::static_type(); spoiler::SpoilerPage::static_type(); + group::GroupPage::static_type(); + picture_group::PictureGroupPage::static_type(); klass.bind_template(); + klass.bind_template_callbacks(); } fn instance_init(obj: &glib::subclass::InitializingObject) { @@ -50,6 +61,19 @@ mod imp { impl ApplicationWindowImpl for SpoilerWindow {} impl AdwApplicationWindowImpl for SpoilerWindow {} + + #[gtk::template_callbacks] + impl SpoilerWindow { + #[template_callback] + pub(super) fn open_sidebar(&self) { + self.leaflet.set_visible_child(&*self.sidebar); + } + + #[template_callback] + pub(super) fn open_content(&self) { + self.leaflet.set_visible_child(&*self.content); + } + } } glib::wrapper! { @@ -63,9 +87,10 @@ impl SpoilerWindow { let obj: Self = glib::Object::builder().property("application", app).build(); if let Some(name) = page { - let pages: Vec<_> = obj - .imp() - .stack + let imp = obj.imp(); + let stack = &*imp.stack; + + let pages: Vec<_> = stack .pages() .iter::() .filter_map(|res| res.ok()) @@ -73,7 +98,8 @@ impl SpoilerWindow { .collect(); if pages.iter().any(|n| n == &name) { - obj.imp().stack.set_visible_child_name(&name); + stack.set_visible_child_name(&name); + imp.open_content(); } else { eprintln!("Page {name} is not available"); eprintln!("Supported pages: {}", pages.join(", ")); diff --git a/demo/src/window/picture_group/mod.rs b/demo/src/window/picture_group/mod.rs new file mode 100644 index 0000000..b30ea31 --- /dev/null +++ b/demo/src/window/picture_group/mod.rs @@ -0,0 +1,68 @@ +use adw::subclass::prelude::*; +use glib::clone; +use gtk::{gdk, glib}; + +mod imp { + + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "src/window/picture_group/picture_group.blp")] + pub struct PictureGroupPage { + #[template_child] + pub(super) group: TemplateChild, + #[template_child] + pub(super) drop_target: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PictureGroupPage { + const NAME: &'static str = "OriDemoPictureGroupPage"; + type Type = super::PictureGroupPage; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PictureGroupPage { + fn constructed(&self) { + self.parent_constructed(); + + self.drop_target.connect_drop( + clone!(@to-owned self as imp => @default-return false, move + |_, value, _, _ | { + let Ok(file_list) = value.get::() else { return false; }; + + let files = file_list.files(); + + let pictures = files.iter().map(|file| { + gtk::Picture::builder() + .file(file) + .content_fit(gtk::ContentFit::Cover) + .overflow(gtk::Overflow::Hidden) + .css_classes(["card"]) + .build() + }); + + imp.group.replace_children(pictures); + + true + } + ), + ); + } + } + impl WidgetImpl for PictureGroupPage {} + impl BinImpl for PictureGroupPage {} +} + +glib::wrapper! { + pub struct PictureGroupPage(ObjectSubclass) + @extends gtk::Widget; +} diff --git a/demo/src/window/picture_group/picture_group.blp b/demo/src/window/picture_group/picture_group.blp new file mode 100644 index 0000000..14aa061 --- /dev/null +++ b/demo/src/window/picture_group/picture_group.blp @@ -0,0 +1,93 @@ +using Gtk 4.0; +using Adw 1; + +template $OriDemoPictureGroupPage: Adw.Bin { + child: Adw.StatusPage { + DropTarget drop_target { + actions: copy; + formats: "GdkFileList"; + } + + title: "Picture Group"; + description: "Drop pictures here"; + + Adw.Clamp { + child: Box { + orientation: vertical; + spacing: 16; + + Adw.SpinRow { + styles [ + "card" + ] + + title: "Spacing"; + + adjustment: Adjustment spacing_adjustment { + lower: 0; + upper: 16; + step-increment: 1; + }; + } + + $OriGroup group { + spacing: bind spacing_adjustment.value; + + Adw.Bin { + styles [ + "card" + ] + + width-request: 6; + height-request: 5; + } + + Adw.Bin { + styles [ + "card" + ] + + width-request: 6; + height-request: 7; + } + + Adw.Bin { + styles [ + "card" + ] + + width-request: 8; + height-request: 10; + } + + Adw.Bin { + styles [ + "card" + ] + + width-request: 6; + height-request: 7; + } + + Adw.Bin { + styles [ + "card" + ] + + width-request: 8; + height-request: 5; + } + + Adw.Bin { + styles [ + "card" + ] + + width-request: 6; + height-request: 8; + } + } + }; + } + }; +} diff --git a/demo/src/window/window.blp b/demo/src/window/window.blp index 837d069..86f376b 100644 --- a/demo/src/window/window.blp +++ b/demo/src/window/window.blp @@ -11,7 +11,9 @@ template $SpoilerWindow : Adw.ApplicationWindow { hexpand: true; vexpand: true; - Box { + visible-child: sidebar; + + Box sidebar { orientation: vertical; vexpand: true; @@ -25,6 +27,12 @@ template $SpoilerWindow : Adw.ApplicationWindow { } StackSidebar { + GestureClick { + button: 1; + + released => $open_content(template); + } + vexpand: true; width-request: 270; @@ -36,11 +44,18 @@ template $SpoilerWindow : Adw.ApplicationWindow { orientation: vertical; } - Box { + Box content { orientation: vertical; vexpand: true; Adw.HeaderBar { + [start] + Button { + icon-name: "go-previous-symbolic"; + visible: bind leaflet.folded; + clicked => $open_sidebar(template); + } + [title] Adw.Bin {} @@ -81,6 +96,20 @@ template $SpoilerWindow : Adw.ApplicationWindow { child: $OriDemoShimmerEffectPage {}; } + + StackPage { + name: "group"; + title: "Group"; + + child: $OriDemoGroupPage {}; + } + + StackPage { + name: "picture_group"; + title: "Picture Group"; + + child: $OriDemoPictureGroupPage {}; + } } } } diff --git a/origami/src/group/child_iter.rs b/origami/src/group/child_iter.rs new file mode 100644 index 0000000..896ad3a --- /dev/null +++ b/origami/src/group/child_iter.rs @@ -0,0 +1,26 @@ +use gtk::prelude::*; + +pub struct ChildIter { + current: Option, +} + +impl Iterator for ChildIter { + type Item = gtk::Widget; + + fn next(&mut self) -> Option { + self.current.take().map(|w| { + self.current = w.next_sibling(); + w + }) + } +} + +pub trait WidgetIterExt: WidgetExt { + fn iter_children(&self) -> ChildIter { + ChildIter { + current: self.first_child(), + } + } +} + +impl WidgetIterExt for T where T: WidgetExt {} diff --git a/origami/src/group/default.rs b/origami/src/group/default.rs new file mode 100644 index 0000000..3f7af52 --- /dev/null +++ b/origami/src/group/default.rs @@ -0,0 +1,166 @@ +use super::*; + +use adw::prelude::*; +use gtk::glib; +use gtk::subclass::prelude::*; + +mod imp { + use super::*; + use std::cell::Cell; + + #[derive(Default, glib::Properties)] + #[properties(wrapper_type = default::Group)] + pub struct Group { + #[property(get, set = Self::set_spacing)] + pub(super) spacing: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for Group { + const NAME: &'static str = "OriGroup"; + type Type = default::Group; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + klass.set_css_name("group"); + } + } + + impl ObjectImpl for Group { + fn properties() -> &'static [glib::ParamSpec] { + Self::derived_properties() + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec) + } + + fn dispose(&self) { + self.obj().remove_children(); + } + } + + impl WidgetImpl for Group { + fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) { + if for_size > 1 << 16 { + return (-1, -1, -1, -1); + } + + let widget = self.obj(); + + if self.less_than_two_children() { + return if let Some(child) = widget.first_child() { + child.measure(orientation, for_size) + } else { + (0, 0, -1, -1) + }; + } + + // TODO: just save aspect ratio and layout + let layout = shared::layout(widget.as_ref().upcast_ref()); + + let last_frame = layout.last().unwrap().layout_frame.get(); + + let aspect_ratio = { + let layout_width = last_frame.0 + last_frame.2; + let layout_height = last_frame.1 + last_frame.3; + layout_width / layout_height + }; + + if for_size == -1 { + let size = if orientation == gtk::Orientation::Vertical { + // height + shared::TARGET_WIDTH / aspect_ratio + } else { + shared::TARGET_WIDTH + }; + return (0, size.round() as i32, -1, -1); + }; + + let size = if orientation == gtk::Orientation::Vertical { + let width = for_size as f32; + width / aspect_ratio + } else { + let heigth = for_size as f32; + heigth * aspect_ratio + } + .round() as i32; + + if orientation == gtk::Orientation::Vertical { + (size, size, -1, -1) + } else { + (0, size, -1, -1) + } + } + + fn request_mode(&self) -> gtk::SizeRequestMode { + gtk::SizeRequestMode::HeightForWidth + } + + fn size_allocate(&self, width: i32, height: i32, baseline: i32) { + let widget = self.obj(); + + if self.less_than_two_children() { + if let Some(child) = widget.first_child() { + child.allocate(width, height, baseline, None); + } + } else { + let layout = shared::layout(widget.as_ref().upcast_ref()); + + let scale = width as f32 / 480.0; + let spacing = self.spacing.get() as f32; + + for (widget, cw) in widget.iter_children().zip(layout.iter()) { + cw.apply(&widget, scale, spacing); + } + } + } + } + + impl Group { + fn set_spacing(&self, spacing: i32) { + self.spacing.set(spacing); + self.obj().queue_allocate(); + } + + fn less_than_two_children(&self) -> bool { + if let Some(first) = self.obj().first_child() { + first.next_sibling().is_none() + } else { + true + } + } + } +} + +glib::wrapper! { + pub struct Group(ObjectSubclass) + @extends gtk::Widget; +} + +impl Group { + pub fn remove_children(&self) { + while let Some(child) = self.first_child() { + child.unparent(); + } + } + + pub fn append(&self, child: &impl IsA) { + child.insert_before(self, gtk::Widget::NONE); + } + + pub fn replace_children(&self, children: I) + where + I: IntoIterator, + W: IsA, + { + self.remove_children(); + for child in children { + self.append(&child); + } + } +} diff --git a/origami/src/group/layout_helpers/item.rs b/origami/src/group/layout_helpers/item.rs new file mode 100644 index 0000000..bc1135b --- /dev/null +++ b/origami/src/group/layout_helpers/item.rs @@ -0,0 +1,97 @@ +use super::position_flags::PositionFlags; +use std::cell::Cell; + +use gtk::gsk; +use gtk::{graphene, prelude::*}; + +#[derive(Debug, Clone)] +pub(crate) struct LayoutItem { + pub(crate) aspect_ratio: f32, + pub(crate) layout_frame: Cell<(f32, f32, f32, f32)>, + pub(crate) position_flags: Cell, +} + +impl LayoutItem { + pub fn new(widget: gtk::Widget) -> Self { + let aspect_ratio = if widget.has_property("aspect-ratio", Some(f32::static_type())) { + // get rid of the warning + _ = widget.measure(gtk::Orientation::Vertical, -1); + widget.property("aspect-ratio") + } else { + let (_min, natural) = widget.preferred_size(); + + natural.width().max(1) as f32 / natural.height().max(1) as f32 + }; + + Self { + aspect_ratio, + layout_frame: Cell::default(), + position_flags: Default::default(), + } + } + + pub fn apply(&self, widget: >k::Widget, scale: f32, spacing: f32) { + let (mut shift_x, mut shift_y, mut width, mut height) = self.layout_frame.get(); + + // Apply scale + shift_x *= scale; + shift_y *= scale; + width *= scale; + height *= scale; + + // Apply spacing + let pos_flags = self.position_flags.get(); + + let half_spacing = spacing * 0.5; + + if !pos_flags.at_left() { + shift_x += half_spacing; + width -= half_spacing; + } + if !pos_flags.at_top() { + shift_y += half_spacing; + height -= half_spacing; + } + if !pos_flags.at_right() { + width -= half_spacing; + } + if !pos_flags.at_bottom() { + height -= half_spacing; + } + + // Otherwise values would be rounded wrong way + width = (shift_x + width).round() - shift_x.round(); + height = (shift_y + height).round() - shift_y.round(); + shift_x = shift_x.round(); + shift_y = shift_y.round(); + + // Remove classes + for class in ["left", "top", "right", "bottom"] { + widget.remove_css_class(class); + } + + let mut classes = [None; 4]; + + if pos_flags.at_left() { + classes[0] = Some("left"); + } + if pos_flags.at_top() { + classes[1] = Some("top"); + } + if pos_flags.at_right() { + classes[2] = Some("right"); + } + if pos_flags.at_bottom() { + classes[3] = Some("bottom"); + } + + for class in classes.into_iter().flatten() { + widget.add_css_class(class); + } + + // Allocate widget + let transform = gsk::Transform::new().translate(&graphene::Point::new(shift_x, shift_y)); + + widget.allocate(width as i32, height as i32, -1, Some(transform)) + } +} diff --git a/origami/src/group/layout_helpers/layout.rs b/origami/src/group/layout_helpers/layout.rs new file mode 100644 index 0000000..d374ced --- /dev/null +++ b/origami/src/group/layout_helpers/layout.rs @@ -0,0 +1,433 @@ +use super::item::LayoutItem; +use super::position_flags::PositionFlags; + +const MIN_WIDTH: f32 = 70.0; + +pub(crate) fn layout_function( + child_count: usize, + force_calc: bool, +) -> fn(children: &[LayoutItem], proportions: &str, average_aspect_ratio: f32, width: f32) { + if force_calc { + layout_fallback + } else { + match child_count { + 2 => layout_two_children, + 3 => layout_three_children, + 4 => layout_four_children, + _ => layout_fallback, + } + } +} + +fn layout_two_children( + children: &[LayoutItem], + proportions: &str, + average_aspect_ratio: f32, + width: f32, +) { + let [child_0, child_1]: &[_; 2] = children.try_into().unwrap(); + + let aspect_ratio_0 = child_0.aspect_ratio; + let aspect_ratio_1 = child_1.aspect_ratio; + + let (layout_frame_0, position_flags_0, layout_frame_1, position_flags_1); + if proportions == "ww" + && average_aspect_ratio > 1.4 + && child_0.aspect_ratio - child_1.aspect_ratio < 0.2 + { + let height = (width / aspect_ratio_0).min(width / aspect_ratio_1); + + layout_frame_0 = (0.0, 0.0, width, height); + position_flags_0 = PositionFlags::TOP | PositionFlags::FULL_WIDTH; + + layout_frame_1 = (0.0, height, width, height); + position_flags_1 = PositionFlags::BOTTOM | PositionFlags::FULL_WIDTH; + } else if matches!(proportions, "ww" | "qq") { + let width = (width) * 0.5; + let height = (width / aspect_ratio_0).min(width / aspect_ratio_1); + + layout_frame_0 = (0.0, 0.0, width, height); + position_flags_0 = PositionFlags::LEFT | PositionFlags::FULL_HEIGHT; + layout_frame_1 = (width, 0.0, width, height); + position_flags_1 = PositionFlags::RIGHT | PositionFlags::FULL_HEIGHT; + } else { + let first_width = + (width) / child_1.aspect_ratio / (1.0 / aspect_ratio_0 + 1.0 / aspect_ratio_1); + let second_width = width - first_width; + + let height = (first_width / aspect_ratio_0).min(second_width / aspect_ratio_1); + + layout_frame_0 = (0.0, 0.0, first_width, height); + position_flags_0 = PositionFlags::LEFT | PositionFlags::FULL_HEIGHT; + layout_frame_1 = (first_width, 0.0, second_width, height); + position_flags_1 = PositionFlags::RIGHT | PositionFlags::FULL_HEIGHT; + }; + + child_0.layout_frame.set(layout_frame_0); + child_0.position_flags.set(position_flags_0); + child_1.layout_frame.set(layout_frame_1); + child_1.position_flags.set(position_flags_1); +} + +fn layout_three_children( + children: &[LayoutItem], + proportions: &str, + average_aspect_ratio: f32, + width: f32, +) { + let [child_0, child_1, child_2]: &[_; 3] = children.try_into().unwrap(); + + let aspect_ratio_0 = child_0.aspect_ratio; + let aspect_ratio_1 = child_1.aspect_ratio; + let aspect_ratio_2 = child_2.aspect_ratio; + + let height = width / average_aspect_ratio; + + let ( + layout_frame_0, + position_flags_0, + layout_frame_1, + position_flags_1, + layout_frame_2, + position_flags_2, + ); + + if proportions.starts_with('n') { + let first_height = height; + + let third_height = ((height) * 0.5) + .min((aspect_ratio_1 * (width) / (aspect_ratio_2 + aspect_ratio_0)).round()); + let second_height = height - third_height; + + let right_width = ((width) * 0.5) + .min( + (third_height * aspect_ratio_2) + .min(second_height * aspect_ratio_1) + .round(), + ) + .max(MIN_WIDTH); + + let left_width = (first_height * aspect_ratio_0) + .min(width - right_width) + .round(); + + layout_frame_0 = (0.0, 0.0, left_width, first_height); + position_flags_0 = PositionFlags::LEFT | PositionFlags::FULL_HEIGHT; + + layout_frame_1 = (left_width, 0.0, right_width, second_height); + position_flags_1 = PositionFlags::TOP_RIGHT; + + layout_frame_2 = (left_width, second_height, right_width, third_height); + position_flags_2 = PositionFlags::BOTTOM_RIGHT; + } else { + let first_height = (width / aspect_ratio_0).min((height) * 0.66).floor(); + + let half_width = (width) * 0.5; + + let second_height = (height - first_height).min( + (half_width / aspect_ratio_1) + .min(half_width / aspect_ratio_2) + .round(), + ); + + layout_frame_0 = (0.0, 0.0, width, first_height); + position_flags_0 = PositionFlags::TOP | PositionFlags::FULL_WIDTH; + + layout_frame_1 = (0.0, first_height, half_width, second_height); + position_flags_1 = PositionFlags::BOTTOM_LEFT; + + layout_frame_2 = (half_width, first_height, half_width, second_height); + + position_flags_2 = PositionFlags::BOTTOM_RIGHT; + }; + + child_0.layout_frame.set(layout_frame_0); + child_0.position_flags.set(position_flags_0); + child_1.layout_frame.set(layout_frame_1); + child_1.position_flags.set(position_flags_1); + child_2.layout_frame.set(layout_frame_2); + child_2.position_flags.set(position_flags_2); +} + +fn layout_four_children( + children: &[LayoutItem], + proportions: &str, + average_aspect_ratio: f32, + width: f32, +) { + let [child_0, child_1, child_2, child_3]: &[_; 4] = children.try_into().unwrap(); + + let aspect_ratio_0 = child_0.aspect_ratio; + let aspect_ratio_1 = child_1.aspect_ratio; + let aspect_ratio_2 = child_2.aspect_ratio; + let aspect_ratio_3 = child_3.aspect_ratio; + + let ( + layout_frame_0, + position_flags_0, + layout_frame_1, + position_flags_1, + layout_frame_2, + position_flags_2, + layout_frame_3, + position_flags_3, + ); + + if proportions.starts_with('w') { + let w = width; + + let h0 = w / aspect_ratio_0; + + layout_frame_0 = (0.0, 0.0, w, h0); + position_flags_0 = PositionFlags::TOP | PositionFlags::FULL_WIDTH; + + let h = (width - 2.0) / (aspect_ratio_1 + aspect_ratio_2 + aspect_ratio_3); + let w0 = ((width - 2.0) * 0.33).max(h * aspect_ratio_1); + let w2 = ((width - 2.0) * 0.33).max(h * aspect_ratio_3); + let w1 = w - w0 - w2 - 2.0; + + let (w1, w2) = if w1 < MIN_WIDTH { + (MIN_WIDTH, w2 - MIN_WIDTH - w1) + } else { + (w1, w2) + }; + + layout_frame_1 = (0.0, h0, w0, h); + position_flags_1 = PositionFlags::BOTTOM_LEFT; + + layout_frame_2 = (w0, h0, w1, h); + position_flags_2 = PositionFlags::BOTTOM; + + layout_frame_3 = (w0 + w1 + 2.0, h0, w2, h); + position_flags_3 = PositionFlags::BOTTOM_RIGHT; + } else { + let height = width / average_aspect_ratio; + + let h: f32 = height; + let left_width: f32 = f32::min(h * aspect_ratio_0, (width) * 0.6); + layout_frame_0 = (0.0, 0.0, left_width, h); + position_flags_0 = PositionFlags::LEFT | PositionFlags::FULL_HEIGHT; + + let w: f32 = + (height - 2.0) / (1.0 / aspect_ratio_1 + 1.0 / aspect_ratio_2 + 1.0 / aspect_ratio_3); + + let h0: f32 = w / aspect_ratio_1; + let h1: f32 = w / aspect_ratio_2; + let h2: f32 = w / aspect_ratio_3; + + let right_width = width - left_width; + + layout_frame_1 = (left_width, 0.0, right_width, h0); + position_flags_1 = PositionFlags::TOP_RIGHT; + + layout_frame_2 = (left_width, h0, right_width, h1); + position_flags_2 = PositionFlags::RIGHT; + + layout_frame_3 = (left_width, h0 + h1 + 2.0, right_width, h2); + position_flags_3 = PositionFlags::BOTTOM_RIGHT; + }; + + child_0.layout_frame.set(layout_frame_0); + child_0.position_flags.set(position_flags_0); + child_1.layout_frame.set(layout_frame_1); + child_1.position_flags.set(position_flags_1); + child_2.layout_frame.set(layout_frame_2); + child_2.position_flags.set(position_flags_2); + child_3.layout_frame.set(layout_frame_3); + child_3.position_flags.set(position_flags_3); +} + +fn layout_fallback( + children: &[LayoutItem], + _proportions: &str, + average_aspect_ratio: f32, + width: f32, +) { + struct GroupedLayoutAttempt { + line_counts: Vec, + heights: Vec, + } + + let cropped_ratios: Vec<_> = children + .iter() + .map(|c| { + if average_aspect_ratio > 1.1 { + c.aspect_ratio.max(1.0) + } else { + c.aspect_ratio.min(1.0) + } + }) + .collect(); + + let multi_height = |ratios: &[f32]| { + let ratio_sum: f32 = ratios.iter().sum(); + (width - (ratios.len() as f32 - 1.0)) / ratio_sum + }; + + let mut attempts = vec![]; + + let mut add_attempt = |line_counts: Vec, heights: Vec| { + attempts.push(GroupedLayoutAttempt { + line_counts, + heights, + }); + }; + + add_attempt(vec![children.len()], vec![multi_height(&cropped_ratios)]); + + { + // Try attempts for different line counts + let mut second_line; + let mut third_line; + let mut fourth_line; + + let len = cropped_ratios.len(); + + for first_line in 1..len { + second_line = len - first_line; + if first_line > 3 || second_line > 3 { + continue; + } + + add_attempt( + vec![first_line, len - first_line], + vec![ + multi_height(&cropped_ratios[..first_line]), + multi_height(&cropped_ratios[first_line..]), + ], + ) + } + + for first_line in 1..len - 1 { + for second_line in 1..len - first_line { + third_line = len - first_line - second_line; + if first_line > 3 + || second_line > (if average_aspect_ratio < 0.85 { 4 } else { 3 }) + || third_line > 3 + { + continue; + } + add_attempt( + vec![first_line, second_line, third_line], + vec![ + multi_height(&cropped_ratios[..first_line]), + multi_height(&cropped_ratios[first_line..len - third_line]), + multi_height(&cropped_ratios[first_line + second_line..]), + ], + ) + } + } + + if len > 2 { + for first_line in 1..len - 2 { + for second_line in 1..len - first_line { + for third_line in 1..len - first_line - second_line { + fourth_line = len - first_line - second_line - third_line; + if first_line > 3 || second_line > 3 || third_line > 3 || fourth_line > 3 { + continue; + } + + add_attempt( + vec![first_line, second_line, third_line, fourth_line], + vec![ + multi_height(&cropped_ratios[..first_line]), + multi_height( + &cropped_ratios[first_line..len - third_line - fourth_line], + ), + multi_height( + &cropped_ratios[first_line + second_line..len - fourth_line], + ), + multi_height( + &cropped_ratios[first_line + second_line + third_line..], + ), + ], + ) + } + } + } + } + } + + let max_height = 600.0; + let mut optimal = None; + let mut optimal_diff = f32::MAX; + + for attempt in attempts { + let mut total_height = 0.0; + let mut min_line_height = f32::MAX; + let mut max_line_height = 0.0; + + for height in &attempt.heights { + total_height += height; + min_line_height = height.min(min_line_height); + max_line_height = height.max(max_line_height); + } + + let mut diff = (total_height - max_height).abs(); + + if attempt.line_counts.len() >= 2 + && ((attempt.line_counts[0] > attempt.line_counts[1]) + || (attempt.line_counts.len() > 2 + && attempt.line_counts[1] > attempt.line_counts[2]) + || (attempt.line_counts.len() > 3 + && attempt.line_counts[2] > attempt.line_counts[3])) + { + diff *= 1.5; + } + + if min_line_height < MIN_WIDTH { + diff *= 1.5; + } + + if diff < optimal_diff { + optimal = Some(attempt); + optimal_diff = diff; + } + } + + let mut index = 0; + let mut y = 0.0; + + let optimal = optimal.unwrap(); + + for i in 0..optimal.line_counts.len() { + let count = optimal.line_counts[i]; + let line_height = optimal.heights[i]; + + let mut x = 0.0; + + let mut position_flags = PositionFlags::NONE; + + if i == 0 { + position_flags |= PositionFlags::TOP; + } else if i == optimal.line_counts.len() - 1 { + position_flags |= PositionFlags::BOTTOM; + } + + for k in 0..count { + let mut inner_position_flags = position_flags; + + if k == 0 { + inner_position_flags |= PositionFlags::LEFT; + } + if k == count - 1 { + inner_position_flags |= PositionFlags::RIGHT; + } + + if position_flags == PositionFlags::NONE { + inner_position_flags |= PositionFlags::INSIDE; + } + + let ratio = cropped_ratios[index]; + let width = ratio * line_height; + + children[index].layout_frame.set((x, y, width, line_height)); + children[index].position_flags.set(inner_position_flags); + + x += width; + index += 1 + } + + y += line_height; + } +} diff --git a/origami/src/group/layout_helpers/mod.rs b/origami/src/group/layout_helpers/mod.rs new file mode 100644 index 0000000..f7b699c --- /dev/null +++ b/origami/src/group/layout_helpers/mod.rs @@ -0,0 +1,6 @@ +mod item; +mod layout; +mod position_flags; + +pub(crate) use item::LayoutItem; +pub(crate) use layout::layout_function; diff --git a/origami/src/group/layout_helpers/position_flags.rs b/origami/src/group/layout_helpers/position_flags.rs new file mode 100644 index 0000000..83ed9cd --- /dev/null +++ b/origami/src/group/layout_helpers/position_flags.rs @@ -0,0 +1,48 @@ +use gtk::glib::bitflags::bitflags; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) struct PositionFlags: u8 { + const NONE = 0; + const INSIDE = 0b000010000; + + const TOP = 0b00000001; + const LEFT = 0b00000010; + const RIGHT = 0b00000100; + const BOTTOM = 0b000001000; + + const TOP_LEFT = Self::TOP.bits() | Self::LEFT.bits(); + const TOP_RIGHT = Self::TOP.bits() | Self::RIGHT.bits(); + const BOTTOM_RIGHT = Self::BOTTOM.bits() | Self::RIGHT.bits(); + const BOTTOM_LEFT = Self::BOTTOM.bits() | Self::LEFT.bits(); + + const FULL_WIDTH = Self::LEFT.bits() | Self::RIGHT.bits(); + const FULL_HEIGHT = Self::TOP.bits() | Self::BOTTOM.bits(); + + const FULL = Self::FULL_WIDTH.bits() | Self::FULL_HEIGHT.bits(); + } +} + +impl Default for PositionFlags { + fn default() -> Self { + Self::NONE + } +} + +impl PositionFlags { + pub fn at_top(self) -> bool { + self.contains(Self::TOP) + } + + pub fn at_right(self) -> bool { + self.contains(Self::RIGHT) + } + + pub fn at_bottom(self) -> bool { + self.contains(Self::BOTTOM) + } + + pub fn at_left(self) -> bool { + self.contains(Self::LEFT) + } +} diff --git a/origami/src/group/mod.rs b/origami/src/group/mod.rs new file mode 100644 index 0000000..225de7e --- /dev/null +++ b/origami/src/group/mod.rs @@ -0,0 +1,10 @@ +mod child_iter; +mod default; +mod layout_helpers; +mod shared; + +use child_iter::*; + +use layout_helpers::*; + +pub use default::Group; diff --git a/origami/src/group/shared.rs b/origami/src/group/shared.rs new file mode 100644 index 0000000..a30d423 --- /dev/null +++ b/origami/src/group/shared.rs @@ -0,0 +1,32 @@ +use super::*; + +pub(super) const TARGET_WIDTH: f32 = 480.0; + +pub(super) fn layout(widget: >k::Widget) -> Vec { + let children: Vec<_> = widget.iter_children().map(LayoutItem::new).collect(); + + let aspect_ratios = children.iter().map(|child| child.aspect_ratio); + + let proportions: String = aspect_ratios + .clone() + .map(|ar| { + if ar > 1.2 { + "w" + } else if ar < 0.8 { + "n" + } else { + "q" + } + }) + .collect(); + + let average_aspect_ratio = aspect_ratios.clone().sum::() / children.len() as f32; + + let force_calc = aspect_ratios.clone().any(|ar| ar > 2.0); + + let layout_function = layout_function(children.len(), force_calc); + + layout_function(&children, &proportions, average_aspect_ratio, TARGET_WIDTH); + + children +} diff --git a/origami/src/lib.rs b/origami/src/lib.rs index a148120..ef20115 100644 --- a/origami/src/lib.rs +++ b/origami/src/lib.rs @@ -1,9 +1,13 @@ //! [Paper Plane](https://github.com/paper-plane-developers/paper-plane) related set of gtk widgets that can be usable outside of it. +mod traits; + +mod group; mod loading_indicator; mod shimmer_effect; mod spoiler_overlay; +pub use group::Group; use gtk::prelude::StaticType; pub use loading_indicator::LoadingIndicator; pub use shimmer_effect::ShimmerEffect; @@ -13,6 +17,7 @@ pub use spoiler_overlay::SpoilerOverlay; /// /// Expected to be called in the main function pub fn init() { + Group::static_type(); LoadingIndicator::static_type(); ShimmerEffect::static_type(); SpoilerOverlay::static_type(); diff --git a/origami/src/shimmer_effect/mod.rs b/origami/src/shimmer_effect/mod.rs index 7c3d940..f9206a0 100644 --- a/origami/src/shimmer_effect/mod.rs +++ b/origami/src/shimmer_effect/mod.rs @@ -90,7 +90,7 @@ mod imp { graphene::Rect::new(win_bounds.x() + shift, win_bounds.y(), GRADIENT_WIDTH, 1.0); let mut color1 = widget.color(); - let mut color2 = color1.clone(); + let mut color2 = color1; color1.set_alpha(0.6); color2.set_alpha(0.3); diff --git a/origami/src/traits/lerp.rs b/origami/src/traits/lerp.rs new file mode 100644 index 0000000..0500201 --- /dev/null +++ b/origami/src/traits/lerp.rs @@ -0,0 +1,34 @@ +pub trait Lerp { + type Output; + fn lerp(self, other: Self, t: F) -> Self::Output; +} + +impl Lerp for f32 { + type Output = Self; + fn lerp(self, other: Self, t: f32) -> Self { + self + t * (other - self) + } +} + +impl Lerp for f64 { + type Output = Self; + fn lerp(self, other: Self, t: f64) -> Self { + self + t * (other - self) + } +} + +impl Lerp for (T, T, T, T) +where + T: Lerp, + F: Copy, +{ + type Output = Self; + fn lerp(self, other: Self, t: F) -> Self { + ( + self.0.lerp(other.0, t), + self.1.lerp(other.1, t), + self.2.lerp(other.2, t), + self.3.lerp(other.3, t), + ) + } +} diff --git a/origami/src/traits/mod.rs b/origami/src/traits/mod.rs new file mode 100644 index 0000000..875f9e0 --- /dev/null +++ b/origami/src/traits/mod.rs @@ -0,0 +1,4 @@ +mod lerp; +// I plan to use it later +#[allow(unused)] +pub(crate) use lerp::Lerp;