Fix #2104: fmt incorrectly using 1-indexing for columns (#2106)

* Fix #2104: fmt incorrectly using 1-indexing for columns

* Clippy...
This commit is contained in:
Jonathan Kelley 2024-03-18 18:23:45 -07:00 committed by GitHub
parent 2dc6cecf2e
commit d8942a255b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 756 additions and 40 deletions

14
Cargo.lock generated
View file

@ -1982,8 +1982,8 @@ name = "dioxus-autofmt"
version = "0.5.0-alpha.2"
dependencies = [
"dioxus-rsx",
"prettier-please",
"pretty_assertions",
"prettyplease",
"proc-macro2",
"quote",
"serde",
@ -2045,7 +2045,7 @@ dependencies = [
"mlua",
"notify",
"open",
"prettier-please",
"prettyplease",
"rayon",
"reqwest",
"rsx-rosetta",
@ -6670,16 +6670,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettier-please"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22020dfcf177fcc7bf5deaf7440af371400c67c0de14c399938d8ed4fb4645d3"
dependencies = [
"proc-macro2",
"syn 2.0.52",
]
[[package]]
name = "pretty_assertions"
version = "1.4.0"

View file

@ -89,16 +89,10 @@ rustc-hash = "1.1.0"
wasm-bindgen = "0.2.92"
html_parser = "0.7.0"
thiserror = "1.0.40"
prettyplease = { package = "prettier-please", version = "0.2", features = [
"verbatim",
] }
manganis-cli-support = { version = "0.2.1", features = [
"html",
] }
prettyplease = { version = "0.2.16", features = ["verbatim"] }
manganis-cli-support = { version = "0.2.1", features = ["html"] }
manganis = { version = "0.2.1" }
interprocess = { version = "1.2.1" }
# interprocess = { git = "https://github.com/kotauskas/interprocess" }
lru = "0.12.2"
async-trait = "0.1.77"

View file

@ -1,4 +1,4 @@
use crate::{ifmt_to_string, writer::Location, Writer};
use crate::{ifmt_to_string, prettier_please::unparse_expr, writer::Location, Writer};
use dioxus_rsx::*;
use quote::ToTokens;
use std::fmt::{Result, Write};
@ -164,7 +164,7 @@ impl Writer<'_> {
let name = &field.name;
match &field.content {
ContentField::ManExpr(exp) => {
let out = prettyplease::unparse_expr(exp);
let out = unparse_expr(exp);
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
write!(self.out, "{name}: {first}")?;
@ -186,7 +186,7 @@ impl Writer<'_> {
write!(self.out, "{}", e.to_token_stream())?;
}
ContentField::OnHandlerRaw(exp) => {
let out = prettyplease::unparse_expr(exp);
let out = unparse_expr(exp);
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
write!(self.out, "{name}: {first}")?;
@ -228,7 +228,7 @@ impl Writer<'_> {
ContentField::Formatted(s) => ifmt_to_string(s).len() ,
ContentField::Shorthand(e) => e.to_token_stream().to_string().len(),
ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
let formatted = prettyplease::unparse_expr(exp);
let formatted = unparse_expr(exp);
let len = if formatted.contains('\n') {
10000
} else {
@ -242,7 +242,7 @@ impl Writer<'_> {
match manual_props {
Some(p) => {
let content = prettyplease::unparse_expr(p);
let content = unparse_expr(p);
if content.len() + attr_len > 80 {
return 100000;
}
@ -264,7 +264,7 @@ impl Writer<'_> {
We want to normalize the expr to the appropriate indent level.
*/
let formatted = prettyplease::unparse_expr(exp);
let formatted = unparse_expr(exp);
let mut lines = formatted.lines();

View file

@ -1,4 +1,4 @@
use crate::{ifmt_to_string, Writer};
use crate::{ifmt_to_string, prettier_please::unparse_expr, Writer};
use dioxus_rsx::*;
use proc_macro2::Span;
use quote::ToTokens;
@ -112,7 +112,7 @@ impl Writer<'_> {
ShortOptimization::Oneliner => {
write!(self.out, " ")?;
self.write_attributes(attributes, key, true)?;
self.write_attributes(brace, attributes, key, true)?;
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
write!(self.out, ", ")?;
@ -132,7 +132,7 @@ impl Writer<'_> {
if !attributes.is_empty() || key.is_some() {
write!(self.out, " ")?;
}
self.write_attributes(attributes, key, true)?;
self.write_attributes(brace, attributes, key, true)?;
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
write!(self.out, ",")?;
@ -145,7 +145,7 @@ impl Writer<'_> {
}
ShortOptimization::NoOpt => {
self.write_attributes(attributes, key, false)?;
self.write_attributes(brace, attributes, key, false)?;
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
write!(self.out, ",")?;
@ -166,6 +166,7 @@ impl Writer<'_> {
fn write_attributes(
&mut self,
brace: &Brace,
attributes: &[AttributeType],
key: &Option<IfmtInput>,
sameline: bool,
@ -187,9 +188,11 @@ impl Writer<'_> {
while let Some(attr) = attr_iter.next() {
self.out.indent_level += 1;
if !sameline {
self.write_comments(attr.start())?;
self.write_attr_comments(brace, attr.start())?;
}
self.out.indent_level -= 1;
if !sameline {
@ -229,7 +232,7 @@ impl Writer<'_> {
write!(
self.out,
"if {condition} {{ ",
condition = prettyplease::unparse_expr(condition),
condition = unparse_expr(condition),
)?;
self.write_attribute_value(value)?;
write!(self.out, " }}")?;
@ -241,7 +244,7 @@ impl Writer<'_> {
write!(self.out, "{value}",)?;
}
ElementAttrValue::AttrExpr(value) => {
let out = prettyplease::unparse_expr(value);
let out = unparse_expr(value);
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
@ -308,7 +311,7 @@ impl Writer<'_> {
fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
write!(self.out, "..")?;
write!(self.out, "{}", prettyplease::unparse_expr(attr))?;
write!(self.out, "{}", unparse_expr(attr))?;
Ok(())
}

View file

@ -27,12 +27,13 @@ impl Writer<'_> {
// If the expr is multiline, we want to collect all of its lines together and write them out properly
// This involves unshifting the first line if it's aligned
let first_line = &self.src[start.line - 1];
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
write!(self.out, "{}", &first_line[start.column..].trim_start())?;
let prev_block_indent_level = self.out.indent.count_indents(first_line);
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
writeln!(self.out)?;
// check if this is the last line
let line = {
if id == (end.line - start.line) - 1 {

View file

@ -17,6 +17,7 @@ mod component;
mod element;
mod expr;
mod indent;
mod prettier_please;
mod writer;
pub use indent::{IndentOptions, IndentType};

View file

@ -0,0 +1,66 @@
use prettyplease::unparse;
use syn::{Expr, File, Item};
/// Unparse an expression back into a string
///
/// This creates a new temporary file, parses the expression into it, and then formats the file.
/// This is a bit of a hack, but dtonlay doesn't want to support this very simple usecase, forcing us to clone the expr
pub fn unparse_expr(expr: &Expr) -> String {
let file = wrapped(expr);
let wrapped = unparse(&file);
unwrapped(wrapped)
}
// Split off the fn main and then cut the tabs off the front
fn unwrapped(raw: String) -> String {
raw.strip_prefix("fn main() {\n")
.unwrap()
.strip_suffix("}\n")
.unwrap()
.lines()
.map(|line| line.strip_prefix(" ").unwrap()) // todo: set this to tab level
.collect::<Vec<_>>()
.join("\n")
}
fn wrapped(expr: &Expr) -> File {
File {
shebang: None,
attrs: vec![],
items: vec![
//
Item::Verbatim(quote::quote! {
fn main() {
#expr
}
}),
],
}
}
#[test]
fn unparses_raw() {
let expr = syn::parse_str("1 + 1").unwrap();
let unparsed = unparse(&wrapped(&expr));
assert_eq!(unparsed, "fn main() {\n 1 + 1\n}\n");
}
#[test]
fn unparses_completely() {
let expr = syn::parse_str("1 + 1").unwrap();
let unparsed = unparse_expr(&expr);
assert_eq!(unparsed, "1 + 1");
}
#[test]
fn weird_ifcase() {
let contents = r##"
fn main() {
move |_| timer.with_mut(|t| if t.started_at.is_none() { Some(Instant::now()) } else { None })
}
"##;
let expr: File = syn::parse_file(contents).unwrap();
let out = unparse(&expr);
println!("{}", out);
}

View file

@ -1,3 +1,4 @@
use crate::prettier_please::unparse_expr;
use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop, IfChain};
use proc_macro2::{LineColumn, Span};
use quote::ToTokens;
@ -5,7 +6,7 @@ use std::{
collections::{HashMap, VecDeque},
fmt::{Result, Write},
};
use syn::{spanned::Spanned, Expr};
use syn::{spanned::Spanned, token::Brace, Expr};
use crate::buffer::Buffer;
use crate::ifmt_to_string;
@ -61,8 +62,26 @@ impl<'a> Writer<'a> {
Some(self.out.buf)
}
pub fn write_attr_comments(&mut self, brace: &Brace, attr_span: Span) -> Result {
// There's a chance this line actually shares the same line as the previous
// Only write comments if the comments actually belong to this line
//
// to do this, we check if the attr span starts on the same line as the brace
// if it doesn't, we write the comments
let brace_line = brace.span.span().start().line;
let attr_line = attr_span.start().line;
if brace_line != attr_line {
self.write_comments(attr_span)?;
}
Ok(())
}
pub fn write_comments(&mut self, child: Span) -> Result {
// collect all comments upwards
// make sure we don't collect the comments of the node that we're currently under.
let start = child.start();
let line_start = start.line - 1;
@ -149,7 +168,7 @@ impl<'a> Writer<'a> {
let len = if let std::collections::hash_map::Entry::Vacant(e) =
self.cached_formats.entry(location)
{
let formatted = prettyplease::unparse_expr(tokens);
let formatted = unparse_expr(tokens);
let len = if formatted.contains('\n') {
10000
} else {
@ -207,7 +226,7 @@ impl<'a> Writer<'a> {
pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
self.cached_formats
.entry(Location::new(expr.span().start()))
.or_insert_with(|| prettyplease::unparse_expr(expr))
.or_insert_with(|| unparse_expr(expr))
.as_str()
}
@ -216,7 +235,7 @@ impl<'a> Writer<'a> {
self.out,
"for {} in {} {{",
forloop.pat.clone().into_token_stream(),
prettyplease::unparse_expr(&forloop.expr)
unparse_expr(&forloop.expr)
)?;
if forloop.body.is_empty() {
@ -249,7 +268,7 @@ impl<'a> Writer<'a> {
self.out,
"{} {} {{",
if_token.to_token_stream(),
prettyplease::unparse_expr(cond)
unparse_expr(cond)
)?;
self.write_body_indented(then_branch)?;

View file

@ -45,4 +45,5 @@ twoway![
tiny,
tinynoopt,
trailing_expr,
many_exprs,
];

View file

@ -0,0 +1,195 @@
#![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()))) }
}
)
}
}
}

View file

@ -26,3 +26,6 @@ twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4, false)
twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4, false)));
twoway!("multiexpr-many" => multiexpr_many (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("simple-combo-expr" => simple_combo_expr (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("oneline-expand" => online_expand (IndentOptions::new(IndentType::Spaces, 4, false)));

View file

@ -0,0 +1,195 @@
#![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()))) }
}
)
}
}
}

View file

@ -0,0 +1,164 @@
#![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()))) }
}
)
}
}
}

View file

@ -0,0 +1,18 @@
fn main() {
rsx! {
button {
id: "start_stop",
onclick: move |_| {
timer
.with_mut(|t| {
t
.started_at = if t.started_at.is_none() {
Some(Instant::now())
} else {
None
};
})
}
}
}
}

View file

@ -0,0 +1,8 @@
fn main() {
rsx! {
button {
id: "start_stop",
onclick: move |_| timer.with_mut(|t| t.started_at = if t.started_at.is_none() { Some(Instant::now()) } else { None } )
}
}
}

View file

@ -0,0 +1,35 @@
fn main() {
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);
}
}
// some comment
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);
}
}
}
}
}

View file

@ -0,0 +1,23 @@
fn main() {
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);
}
}
// some comment
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);
}
}
}
}
}