DioxusLabs / dioxus

Fullstack GUI library for web, desktop, mobile, and more.
https://dioxuslabs.com
Apache License 2.0
19.37k stars 747 forks source link

[dioxus-cli@0.5.0-alpha.0] `dx fmt` breaks file #2104

Closed mh-trimble closed 3 months ago

mh-trimble commented 3 months ago

Problem

When I format the following valid code. I get a file with mismatched curly braces and some extra empty lines I would not expect.

#![allow(dead_code, unused)]
use dioxus::desktop::use_window;
use dioxus::prelude::*;
use std::{
    process::exit,
    time::{Duration, Instant},
};
use tokio::time::sleep;

fn main() {
    LaunchBuilder::desktop().launch(app);
}

struct WindowPreferences {
    always_on_top: bool,
    with_decorations: bool,
    exiting: Option<Instant>,
}

impl Default for WindowPreferences {
    fn default() -> Self {
        Self {
            with_decorations: true,
            always_on_top: false,
            exiting: None,
        }
    }
}

impl WindowPreferences {
    fn new() -> Self {
        Self::default()
    }
}

#[derive(Default)]
struct Timer {
    hours: u8,
    minutes: u8,
    seconds: u8,
    started_at: Option<Instant>,
}

impl Timer {
    fn new() -> Self {
        Self::default()
    }

    fn duration(&self) -> Duration {
        Duration::from_secs(
            (self.hours as u64 * 60 + self.minutes as u64) * 60 + self.seconds as u64,
        )
    }
}

const UPD_FREQ: Duration = Duration::from_millis(100);

fn exit_button(
    delay: Duration,
    label: fn(Signal<Option<Instant>>, Duration) -> Option<VNode>,
) -> Element {
    let mut trigger: Signal<Option<Instant>> = use_signal(|| None);
    use_future(move || async move {
        loop {
            sleep(UPD_FREQ).await;
            if let Some(true) = trigger.read().map(|e| e.elapsed() > delay) {
                exit(0);
            }
        }
    });
    let stuff: Option<VNode> = rsx! {
        button {
            onmouseup: move |_| {
                trigger.set(None);
            },
            onmousedown: move |_| {
                trigger.set(Some(Instant::now()));
            },
            width: 100,
            {label(trigger, delay)},
        }
    };
    stuff
}

fn app() -> Element {
    let mut timer = use_signal(Timer::new);
    let mut window_preferences = use_signal(WindowPreferences::new);

    use_future(move || async move {
        loop {
            sleep(UPD_FREQ).await;
            timer.with_mut(|t| {
                if let Some(started_at) = t.started_at {
                    if t.duration().saturating_sub(started_at.elapsed()) == Duration::ZERO {
                        t.started_at = None;
                    }
                }
            });
        }
    });

    rsx! {
        div {{
            let millis = timer.with(|t| t.duration().saturating_sub(t.started_at.map(|x| x.elapsed()).unwrap_or(Duration::ZERO)).as_millis());
            format!("{:02}:{:02}:{:02}.{:01}",
                    millis / 1000 / 3600 % 3600,
                    millis / 1000 / 60 % 60,
                    millis / 1000 % 60,
                    millis / 100 % 10)
        }}
        div {
            input { r#type: "number", min: 0, max: 99, value: format!("{:02}",timer.read().hours), oninput: move |e| {
                timer.write().hours = e.value().parse().unwrap_or(0);
                }
            }

            input { r#type: "number", min: 0, max: 59, value: format!("{:02}",timer.read().minutes), oninput: move |e| {
                timer.write().minutes = e.value().parse().unwrap_or(0);
                }
            }

            input { r#type: "number", min: 0, max: 59, value: format!("{:02}",timer.read().seconds), oninput: move |e| {
                timer.write().seconds = e.value().parse().unwrap_or(0);
                }
            }
        }

        button {
            id: "start_stop",
            onclick: move |_| timer.with_mut(|t| t.started_at = if t.started_at.is_none() { Some(Instant::now()) } else { None } ),
            { timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" }) },
        }
        div { id: "app",
            button { onclick: move |_| {
                let decorations = window_preferences.read().with_decorations;
                use_window().set_decorations(!decorations);
                window_preferences.write().with_decorations = !decorations;
                }, {
                    format!("with decorations{}", if window_preferences.read().with_decorations { " ✓" } else { "" }).to_string()
                }
            }
            button {
                onclick: move |_| {
                    window_preferences.with_mut(|wp| {
                        use_window().set_always_on_top(!wp.always_on_top);
                        wp.always_on_top = !wp.always_on_top;
                    })},
                width: 100,
                {
                    format!("always on top{}", if window_preferences.read().always_on_top { " ✓" } else { "" })
                }
            }
        }
        {{ exit_button(
             Duration::from_secs(3),
             |trigger, delay| rsx!{{format!("{:0.1?}", trigger.read().map(|inst| (delay.as_secs_f32() - inst.elapsed().as_secs_f32()))) }}
                       )  }}

    }
}

I get the following formatted file

#![allow(dead_code, unused)]
use dioxus::desktop::use_window;
use dioxus::prelude::*;
use std::{
    process::exit,
    time::{Duration, Instant},
};
use tokio::time::sleep;

fn main() {
    LaunchBuilder::desktop().launch(app);
}

struct WindowPreferences {
    always_on_top: bool,
    with_decorations: bool,
    exiting: Option<Instant>,
}

impl Default for WindowPreferences {
    fn default() -> Self {
        Self {
            with_decorations: true,
            always_on_top: false,
            exiting: None,
        }
    }
}

impl WindowPreferences {
    fn new() -> Self {
        Self::default()
    }
}

#[derive(Default)]
struct Timer {
    hours: u8,
    minutes: u8,
    seconds: u8,
    started_at: Option<Instant>,
}

impl Timer {
    fn new() -> Self {
        Self::default()
    }

    fn duration(&self) -> Duration {
        Duration::from_secs(
            (self.hours as u64 * 60 + self.minutes as u64) * 60 + self.seconds as u64,
        )
    }
}

const UPD_FREQ: Duration = Duration::from_millis(100);

fn exit_button(
    delay: Duration,
    label: fn(Signal<Option<Instant>>, Duration) -> Option<VNode>,
) -> Element {
    let mut trigger: Signal<Option<Instant>> = use_signal(|| None);
    use_future(move || async move {
        loop {
            sleep(UPD_FREQ).await;
            if let Some(true) = trigger.read().map(|e| e.elapsed() > delay) {
                exit(0);
            }
        }
    });
    let stuff: Option<VNode> = rsx! {
        button {
            onmouseup: move |_| {
                trigger.set(None);
            },
            onmousedown: move |_| {
                trigger.set(Some(Instant::now()));
            },
            width: 100,
            {label(trigger, delay)}
        }
    };
    stuff
}

fn app() -> Element {
    let mut timer = use_signal(Timer::new);
    let mut window_preferences = use_signal(WindowPreferences::new);

    use_future(move || async move {
        loop {
            sleep(UPD_FREQ).await;
            timer.with_mut(|t| {
                if let Some(started_at) = t.started_at {
                    if t.duration().saturating_sub(started_at.elapsed()) == Duration::ZERO {
                        t.started_at = None;
                    }
                }
            });
        }
    });

    rsx! {
        div {
            {{
                let millis = timer.with(|t| t.duration().saturating_sub(t.started_at.map(|x| x.elapsed()).unwrap_or(Duration::ZERO)).as_millis());
                format!("{:02}:{:02}:{:02}.{:01}",
                        millis / 1000 / 3600 % 3600,
                        millis / 1000 / 60 % 60,
                        millis / 1000 % 60,
                        millis / 100 % 10)
            }
        }
        div {
            input {
                r#type: "number",
                min: 0,
                max: 99,
                value: format!("{:02}", timer.read().hours),
                oninput: move |e| {
                    timer.write().hours = e.value().parse().unwrap_or(0);
                }
            }

            input {

                r#type: "number",

                min: 0,

                max: 59,

                value: format!("{:02}", timer.read().minutes),

                oninput: move |e| {
                    timer.write().minutes = e.value().parse().unwrap_or(0);
                }
            }

            input {

                r#type: "number",

                min: 0,

                max: 59,

                value: format!("{:02}", timer.read().seconds),

                oninput: move |e| {
                    timer.write().seconds = e.value().parse().unwrap_or(0);
                }
            }
        }

        button {
            id: "start_stop",
            onclick: move |_| {
                timer
                    .with_mut(|t| {
                        t
                            .started_at = if t.started_at.is_none() {
                            Some(Instant::now())
                        } else {
                            None
                        };
                    })
            },
            { timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" }) }
        }
        div { id: "app",
            button {
                onclick: move |_| {
                    let decorations = window_preferences.read().with_decorations;
                    use_window().set_decorations(!decorations);
                    window_preferences.write().with_decorations = !decorations;
                },
                {
                    format!("with decorations{}", if window_preferences.read().with_decorations { " ✓" } else { "" }).to_string()
                }
            }
            button {
                onclick: move |_| {
                    window_preferences
                        .with_mut(|wp| {
                            use_window().set_always_on_top(!wp.always_on_top);
                            wp.always_on_top = !wp.always_on_top;
                        })
                },
                width: 100,
                {
                    format!("always on top{}", if window_preferences.read().always_on_top { " ✓" } else { "" })
                }
            }
        }
        {{ exit_button(
            Duration::from_secs(3),
            |trigger, delay| rsx!{{format!("{:0.1?}", trigger.read().map(|inst| (delay.as_secs_f32() - inst.elapsed().as_secs_f32()))) }}
                    )  }}
    }
}

Steps To Reproduce

Steps to reproduce the behavior:

Expected behavior

dx fmt should format the file without breaking it

Environment:

Questionnaire

jkelleyrtp commented 3 months ago

Great test case, caught an issue where exprs were dragging one extra character with them, causing the extra curly.

We can't do much about the nested rsx! case in the file expr though, but everything else should be fixed in #2106