4 Commits

10 changed files with 223 additions and 185 deletions

138
build.rs
View File

@@ -1,4 +1,5 @@
use std::env; use std::env;
use std::fmt::Display;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::io::{self, BufRead}; use std::io::{self, BufRead};
@@ -10,14 +11,32 @@ use regex::Regex;
fn main() { fn main() {
// Tell Cargo to rerun this build script if any SCSS file // Tell Cargo to rerun this build script if any SCSS file
// in the 'src' directory or its subdirectories changes. // in the 'src' directory or its subdirectories changes.
println!("cargo:rerun-if-changed=src/**/*.scss"); println!("cargo:rerun-if-changed=src/ui/**/*.scss");
let out_dir = env::var("OUT_DIR").unwrap(); let out_dir = env::var("OUT_DIR").unwrap();
let style_src_path = PathBuf::from("src/ui/_base.scss"); let mut tasks = Vec::new();
let style_dst_path = Path::new(&out_dir).join("style_vars.rs");
export_color_variables(&style_src_path, &style_dst_path) // Global tokens
tasks.push(Task::new(
PathBuf::from("src/ui/_base.scss"),
Path::new(&out_dir).join("style_tokens.rs"),
"style".to_string(),
));
// variables defined by the Panel component
tasks.push(Task::new(
PathBuf::from("src/ui/components/_panel.scss"),
Path::new(&out_dir).join("style_component_panel.rs"),
"panel".to_string(),
));
// Variables set by the Conversations layout
tasks.push(Task::new(
PathBuf::from("src/ui/layouts/conversations.scss"),
Path::new(&out_dir).join("style_layout_conversations.rs"),
"conversations".to_string(),
));
export_variables(tasks)
} }
// From https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html // From https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html
@@ -32,14 +51,21 @@ where
} }
#[derive(Debug)] #[derive(Debug)]
struct CssColorVariable<'a> { struct ColorVariable {
name: &'a str, name: String,
value: &'a str, value: String,
} }
impl<'a> CssColorVariable<'a> { impl ColorVariable {
pub fn to_rust(&self) -> String { pub fn new(name: String, value: String) -> Self {
format!( Self { name, value }
}
}
impl Display for ColorVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"const {name}: &str = \"{value}\";", "const {name}: &str = \"{value}\";",
name = self.name.replace('-', "_").to_uppercase(), name = self.name.replace('-', "_").to_uppercase(),
value = self.value value = self.value
@@ -47,35 +73,83 @@ impl<'a> CssColorVariable<'a> {
} }
} }
fn export_color_variables(src_path: &PathBuf, dst_path: &PathBuf) { #[derive(Debug)]
let mut dst_file = File::create(dst_path).unwrap(); struct FloatVariable {
if let Err(err) = dst_file.write(b"#[allow(dead_code)]\nmod style {") { name: String,
println!("{}", err); value: f64,
return; }
};
let re = Regex::new(r"^\$([^:]+):[[:space:]]*#([^$]+);[[:space:]]*$").unwrap(); impl FloatVariable {
pub fn new(name: String, value: f64) -> Self {
Self { name, value }
}
}
if let Ok(lines) = read_lines(src_path) { impl Display for FloatVariable {
for line in lines.map_while(Result::ok) { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Some(groups) = re.captures(&line) else { write!(
continue; f,
}; "const {name}: f64 = {value};",
name = self.name.replace('-', "_").to_uppercase(),
value = self.value
)
}
}
let var = CssColorVariable { struct Task {
name: &groups[1], src_path: PathBuf,
value: &groups[2], dst_path: PathBuf,
}; module_name: String,
}
let rust_export = var.to_rust(); impl Task {
if let Err(err) = dst_file.write_fmt(format_args!(" pub {}\n", rust_export)) { pub fn new(src_path: PathBuf, dst_path: PathBuf, module_name: String) -> Self {
Self {
src_path,
dst_path,
module_name,
}
}
}
// fn export_variables(src_path: &PathBuf, dst_path: &PathBuf) {
fn export_variables(tasks: Vec<Task>) {
let color_re = Regex::new(r"^\$([^:]+):[[:space:]]*#([^$]+);[[:space:]]*$").unwrap();
let variable_re = Regex::new(r"^\$([^:]+):[[:space:]]*([^;]+)[[:space:]]*;").unwrap();
for task in tasks {
let mut dst_file = File::create(task.dst_path).unwrap();
if let Err(err) = dst_file.write_fmt(format_args!(
"#[allow(dead_code)]\nmod {} {{\n",
task.module_name
)) {
println!("{}", err);
return;
};
let mut variables = Vec::<Box<dyn Display>>::new();
if let Ok(lines) = read_lines(task.src_path) {
for line in lines.map_while(Result::ok) {
if let Some(groups) = color_re.captures(&line) {
let var = ColorVariable::new(groups[1].to_string(), groups[2].to_string());
variables.push(Box::new(var));
} else if let Some(groups) = variable_re.captures(&line) {
if let Ok(value) = groups[2].parse::<f64>() {
variables.push(Box::new(FloatVariable::new(groups[1].to_string(), value)));
}
}
}
}
for variable in variables {
if let Err(err) = dst_file.write_fmt(format_args!(" pub {}\n", variable)) {
println!("{}", err); println!("{}", err);
break; break;
} }
} }
}
if let Err(err) = dst_file.write(b"}\n") { if let Err(err) = dst_file.write(b"}\n") {
println!("{}", err); println!("{}", err);
}; };
}
} }

View File

@@ -1,6 +1,6 @@
@import "../base.scss"; @import "../base.scss";
$panel-aspect-ratio: 1/1.618; $aspect-ratio: 0.618; // 1/1.618;
@mixin panel($padding-v: 2%, $padding-h: 2%) { @mixin panel($padding-v: 2%, $padding-h: 2%) {
padding: $padding-v $padding-h; padding: $padding-v $padding-h;

View File

@@ -284,102 +284,39 @@ pub fn ConversationsCarousel(on_selected_conversation: EventHandler<RoomId>) ->
} }
} }
// If id is None, the Space will handle all the Conversation which have no parent (Space).
#[component] #[component]
pub fn Space(id: SpaceId) -> Element { pub fn Space(id: Option<SpaceId>) -> Element {
let space = STORE.read().spaces().get(&id).unwrap().signal();
let name = space.name();
let mut selected_room_id = use_context_provider(|| Signal::new(None::<RoomId>)); let mut selected_room_id = use_context_provider(|| Signal::new(None::<RoomId>));
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::new())); let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::new()));
use_effect(move || { let name = if let Some(id) = id {
// let rooms = STORE.read().rooms(); let space = STORE.read().spaces().get(&id).unwrap().signal();
let rooms = STORE.peek().rooms(); use_effect(move || {
let room_ids = space.room_ids(); let rooms = STORE.peek().rooms();
for room_id in room_ids { let room_ids = space.room_ids();
if rooms.contains_key(&room_id) { for room_id in room_ids {
displayed_rooms.write().push(room_id); if rooms.contains_key(&room_id) {
displayed_rooms.write().push(room_id);
}
} }
} });
}); space.name()
} else {
let on_selected_conversation = move |room_id: RoomId| { use_effect(move || {
trace!(""); let rooms = STORE.read().rooms();
selected_room_id.set(Some(room_id)); for room in rooms.values() {
if room.signal().spaces().is_empty() {
let room_id = room.signal().id();
displayed_rooms.write().push(room_id);
}
}
});
Some("Home".to_string())
}; };
let mut space_classes: [&str; 2] = [ClassName::SPACE, ""];
let mut selected_room_name = "".to_string();
if let Some(room_id) = selected_room_id.read().as_ref() {
space_classes[1] = ClassName::DISPLAY_CONVERSATION_NAME;
if let Some(room) = STORE.read().rooms().get(room_id) {
let room = room.signal();
if let Some(name) = room.name() {
selected_room_name = name;
} else {
debug!("No name set for {} room", &room_id);
selected_room_name = room_id.to_string();
}
} else {
warn!("No room found for the {} id", &room_id);
}
}
let classes_str = space_classes.join(" ");
rsx! {
style { {STYLE_SHEET} },
div {
class: "{classes_str}",
// Deselect the conversation on clicks outside of the ConversationAvatar
onclick: move |_| {
selected_room_id.set(None);
},
div {
class: ClassName::SPACE_NAME,
p {
{name}
}
},
ConversationsCarousel {
on_selected_conversation,
},
div {
class: ClassName::SPACE_CONVERSATION_NAME,
p {
{selected_room_name},
}
}
}
}
}
#[component]
pub fn HomeSpace() -> Element {
let name = "Home";
let mut selected_room_id = use_context_provider(|| Signal::new(None::<RoomId>));
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::new()));
use_effect(move || {
let rooms = STORE.read().rooms();
for room in rooms.values() {
if room.signal().spaces().is_empty() {
let room_id = room.signal().id();
displayed_rooms.write().push(room_id);
}
}
});
let on_selected_conversation = move |room_id: RoomId| { let on_selected_conversation = move |room_id: RoomId| {
STORE.write().on_selected_room(room_id.clone());
selected_room_id.set(Some(room_id)); selected_room_id.set(Some(room_id));
}; };
@@ -454,7 +391,7 @@ pub fn Spaces() -> Element {
{rendered_spaces}, {rendered_spaces},
HomeSpace {}, Space {},
} }
} }
} }

View File

@@ -7,7 +7,7 @@ use dioxus_free_icons::{Icon, IconShape};
turf::style_sheet!("src/ui/components/icons.scss"); turf::style_sheet!("src/ui/components/icons.scss");
include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100}; use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100};

View File

@@ -19,7 +19,7 @@ use super::{
text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState}, text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState},
}; };
include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
use style::{ use style::{
COLOR_PRIMARY_100, COLOR_PRIMARY_110, COLOR_PRIMARY_120, COLOR_PRIMARY_140, COLOR_PRIMARY_150, COLOR_PRIMARY_100, COLOR_PRIMARY_110, COLOR_PRIMARY_120, COLOR_PRIMARY_140, COLOR_PRIMARY_150,

View File

@@ -9,7 +9,7 @@ use crate::infrastructure::services::random_svg_generators::{
generate_random_svg_avatar, AvatarConfig, AvatarFeeling, generate_random_svg_avatar, AvatarConfig, AvatarFeeling,
}; };
include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100}; use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100};

View File

@@ -2,16 +2,23 @@ use std::rc::Rc;
use dioxus::prelude::*; use dioxus::prelude::*;
use futures::join; use futures::join;
use tracing::{error, warn};
use crate::ui::{
components::{
chat_panel::ChatPanel, conversations::Conversations as ConversationsComponent,
wallpaper::Wallpaper,
},
STORE,
};
turf::style_sheet!("src/ui/layouts/conversations.scss"); turf::style_sheet!("src/ui/layouts/conversations.scss");
use crate::ui::components::chat_panel::ChatPanel; include!(concat!(env!("OUT_DIR"), "/style_component_panel.rs"));
use crate::ui::components::conversations::Conversations as ConversationsComponent; include!(concat!(env!("OUT_DIR"), "/style_layout_conversations.rs"));
use crate::ui::components::wallpaper::Wallpaper;
// TODO: Get from SCSS use conversations::INNER_PANEL_HEIGHT_RATIO;
const WIDGET_HEIGHT_RATIO: f64 = 0.95; use panel::ASPECT_RATIO;
const ASPECT_RATIO: f64 = 1.0 / 1.618;
async fn on_carousel_scroll( async fn on_carousel_scroll(
parent_div: &Rc<MountedData>, parent_div: &Rc<MountedData>,
@@ -44,36 +51,50 @@ async fn on_carousel_scroll(
} }
fn LayoutSmall() -> Element { fn LayoutSmall() -> Element {
let mut carousel_div = use_signal(|| None::<Rc<MountedData>>);
let mut first_div = use_signal(|| None::<Rc<MountedData>>); let mut first_div = use_signal(|| None::<Rc<MountedData>>);
let mut last_div = use_signal(|| None::<Rc<MountedData>>); let mut last_div = use_signal(|| None::<Rc<MountedData>>);
let mut carousel_div = use_signal(|| None::<Rc<MountedData>>);
let conversation_panels_nb = 3; let displayed_room_ids = STORE.read().displayed_room_ids();
let conversation_panels = (0..conversation_panels_nb + 1).map(|i| {
let inner = rsx! {
div {
class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL_INNER,
ChatPanel { name: format!("CHAT #{i}") },
}
};
if i == conversation_panels_nb { let mut conversation_panels = Vec::new();
rsx! { let mut displayed_room_ids_it = displayed_room_ids.iter().peekable();
div { while let Some(room_id) = displayed_room_ids_it.next() {
class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL, if let Some(room) = STORE.read().rooms().get(room_id) {
onmounted: move |cx: Event<MountedData>| last_div.set(Some(cx.data())), let room = room.signal();
{inner} let room_name_repr = room.name().unwrap_or(room.id().to_string());
let inner = rsx! {
div {
class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL_INNER,
ChatPanel { name: format!("CHAT {room_name_repr}") },
}
};
// If this is the last iteration
let panel = if displayed_room_ids_it.peek().is_none() {
rsx! {
div {
class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL,
onmounted: move |cx: Event<MountedData>| last_div.set(Some(cx.data())),
{inner}
}
} }
} else {
rsx! {
div {
class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL,
{inner}
}
}
};
if let Some(panel) = panel {
conversation_panels.push(panel);
} }
} else { } else {
rsx! { warn!("No {} room found", room_id);
div {
class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL,
{inner}
}
}
} }
}); }
rsx! { rsx! {
style { {STYLE_SHEET} }, style { {STYLE_SHEET} },
@@ -116,7 +137,7 @@ fn LayoutSmall() -> Element {
}, },
}, },
{conversation_panels} {conversation_panels.iter()}
div { div {
class: ClassName::CONVERSATIONS_VIEW_TAIL, class: ClassName::CONVERSATIONS_VIEW_TAIL,
@@ -210,22 +231,7 @@ fn LayoutBig() -> Element {
} }
pub fn Conversations() -> Element { pub fn Conversations() -> Element {
let mut view_size = use_signal(|| None::<(f64, f64)>); let mut layout = use_signal(|| None::<VNode>);
// TODO: Make the layout reactive (on window resize)
let layout = {
move || {
if let Some((width, height)) = view_size.read().as_ref() {
let component_width = height * WIDGET_HEIGHT_RATIO * ASPECT_RATIO;
let breakpoint_width = component_width * 2_f64;
if *width >= breakpoint_width {
return rsx! { LayoutBig {} };
}
}
rsx! {LayoutSmall {}}
}
}();
rsx! { rsx! {
style { {STYLE_SHEET} }, style { {STYLE_SHEET} },
@@ -236,14 +242,21 @@ pub fn Conversations() -> Element {
div { div {
class: ClassName::CONVERSATIONS_VIEW, class: ClassName::CONVERSATIONS_VIEW,
onmounted: move |cx| {
async move {
let data = cx.data();
if let Ok(client_rect) = data.get_client_rect().await { onresized: move |cx| {
view_size.set(Some((client_rect.size.width, client_rect.size.height))); let data = cx.data();
let mut use_big_layout = false;
if let Ok(size) = data.get_border_box_size() {
if let Some(size) = size.first() {
// Use LayoutBig if the layout can contain 2 panels side by side
let component_width = size.height * INNER_PANEL_HEIGHT_RATIO * ASPECT_RATIO;
let breakpoint_width = component_width * 2_f64;
use_big_layout = size.width > breakpoint_width;
} }
} }
layout.set(rsx! { if use_big_layout { LayoutBig {} } else { LayoutSmall {} }});
}, },
{layout} {layout}

View File

@@ -17,11 +17,13 @@
scroll-snap-align: start; scroll-snap-align: start;
} }
$inner-panel-height-ratio: 0.95;
.conversations-view { .conversations-view {
$height: 100vh; $height: 100vh;
$width: 100vw; $width: 100vw;
$conversations-panel-height: calc($height * 0.95); $conversations-panel-height: calc($height * $inner-panel-height-ratio);
$conversations-panel-width: calc($conversations-panel-height * $panel-aspect-ratio); $conversations-panel-width: calc($conversations-panel-height * $aspect-ratio);
$gap: 1%; $gap: 1%;
$content-height: 95%; $content-height: 95%;
$ratio: 2; $ratio: 2;
@@ -63,11 +65,11 @@
height: 100%; height: 100%;
// TODO: Is aspect-ratio the best criteria to defined that inner shall take all the available space ? // TODO: Is aspect-ratio the best criteria to defined that inner shall take all the available space ?
@media (max-aspect-ratio: $panel-aspect-ratio) { @media (max-aspect-ratio: $aspect-ratio) {
width: 100%; width: 100%;
} }
@media (min-aspect-ratio: $panel-aspect-ratio) { @media (min-aspect-ratio: $aspect-ratio) {
aspect-ratio: $panel-aspect-ratio; aspect-ratio: $aspect-ratio;
} }
} }
} }
@@ -105,7 +107,7 @@
&__conversations-panel { &__conversations-panel {
height: $content-height; height: $content-height;
aspect-ratio: $panel-aspect-ratio; aspect-ratio: $aspect-ratio;
} }
&__conversation-panels { &__conversation-panels {

View File

@@ -18,14 +18,14 @@
align-items: safe center; align-items: safe center;
&__login-panel { &__login-panel {
@media (max-aspect-ratio: $panel-aspect-ratio) { @media (max-aspect-ratio: $aspect-ratio) {
width: 95%; width: 95%;
} }
@media (min-aspect-ratio: $panel-aspect-ratio) { @media (min-aspect-ratio: $aspect-ratio) {
height: 100%; height: 100%;
} }
aspect-ratio: $panel-aspect-ratio; aspect-ratio: $aspect-ratio;
max-height: $panel-max-height; max-height: $panel-max-height;
flex-shrink: 0; flex-shrink: 0;
@@ -36,6 +36,6 @@
justify-content: center; justify-content: center;
// Variables inherited by children // Variables inherited by children
--aspect-ratio: #{$panel-aspect-ratio}; --aspect-ratio: #{$aspect-ratio};
} }
} }

View File

@@ -1,6 +1,7 @@
pub(crate) mod room; pub(crate) mod room;
pub(crate) mod space; pub(crate) mod space;
use std::collections::HashSet;
use std::{collections::HashMap, rc::Rc}; use std::{collections::HashMap, rc::Rc};
use async_trait::async_trait; use async_trait::async_trait;
@@ -21,6 +22,17 @@ use space::Space;
pub struct Store { pub struct Store {
rooms: HashMap<RoomId, Rc<Room>>, rooms: HashMap<RoomId, Rc<Room>>,
spaces: HashMap<SpaceId, Rc<Space>>, spaces: HashMap<SpaceId, Rc<Space>>,
displayed_room_ids: HashSet<RoomId>,
}
impl Store {
pub fn on_selected_room(&mut self, room_id: RoomId) {
// Toggle the room_id selection
if !self.displayed_room_ids.write().remove(&room_id) {
self.displayed_room_ids.write().insert(room_id);
}
}
} }
#[async_trait(?Send)] #[async_trait(?Send)]