Merge branch 'master' into jk/global-window-fun

This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-01-04 10:22:10 -08:00
commit 799be5e4e3
200 files changed
+6082 -2375

No files matched your search

+4 -7
View File
@@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
};
cx.render(rsx! (
div {
style: "{CONTAINER_STYLE}",
div { style: "{CONTAINER_STYLE}",
div {
style: "{RECT_STYLE}",
// focusing is necessary to catch keyboard events
@@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
onmousemove: move |event| log_event(Event::MouseMove(event)),
onclick: move |event| log_event(Event::MouseClick(event)),
ondblclick: move |event| log_event(Event::MouseDoubleClick(event)),
ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event)),
onmousedown: move |event| log_event(Event::MouseDown(event)),
onmouseup: move |event| log_event(Event::MouseUp(event)),
@@ -77,9 +76,7 @@ fn app(cx: Scope) -> Element {
"Hover, click, type or scroll to see the info down below"
}
div {
events.read().iter().map(|event| rsx!( div { "{event:?}" } ))
},
},
div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
}
))
}
+1
View File
@@ -62,6 +62,7 @@ fn app(cx: Scope) -> Element {
div { id: "wrapper",
div { class: "app",
div { class: "calculator",
tabindex: "0",
onkeydown: handle_key_down_event,
div { class: "calculator-display", val.to_string() }
div { class: "calculator-keypad",
+2 -8
View File
@@ -25,14 +25,8 @@ fn app(cx: Scope) -> Element {
button {
onclick: move |_| {
let dom = VirtualDom::new_with_props(compose, ComposeProps {
app_tx: tx.clone()
});
// this returns a weak reference to the other window
// Be careful not to keep a strong reference to the other window or it will never be dropped
// and the window will never close.
dioxus_desktop::window().new_window(dom, Default::default());
let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
window.new_window(dom, Default::default());
},
"Click to compose a new email"
}
+29
View File
@@ -0,0 +1,29 @@
use dioxus::prelude::*;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use std::path::Path;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, |request: &AssetRequest| {
let path = request.path().to_path_buf();
async move {
if path != Path::new("logo.png") {
return None;
}
let image_data: &[u8] = include_bytes!("./assets/logo.png");
Some(Response::new(image_data.into()))
}
});
cx.render(rsx! {
div {
img {
src: "logo.png"
}
}
})
}
+1 -1
View File
@@ -35,7 +35,7 @@ frameworks = ["WebKit"]
[dependencies]
anyhow = "1.0.56"
log = "0.4.11"
wry = "0.28.0"
wry = "0.34.0"
dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop", features = [
"tokio_runtime",
+1
View File
@@ -2,6 +2,7 @@
name = "openid_auth_demo"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+20 -20
View File
@@ -1,20 +1,20 @@
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Discovery error: {0}")]
OpenIdConnect(
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
),
#[error("Parsing error: {0}")]
Parse(#[from] url::ParseError),
#[error("Request token error: {0}")]
RequestToken(
#[from]
RequestTokenError<
openidconnect::reqwest::Error<reqwest::Error>,
StandardErrorResponse<CoreErrorResponseType>,
>,
),
}
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Discovery error: {0}")]
OpenIdConnect(
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
),
#[error("Parsing error: {0}")]
Parse(#[from] url::ParseError),
#[error("Request token error: {0}")]
RequestToken(
#[from]
RequestTokenError<
openidconnect::reqwest::Error<reqwest::Error>,
StandardErrorResponse<CoreErrorResponseType>,
>,
),
}
+12
View File
@@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
a: "asd".to_string(),
c: "asd".to_string(),
d: Some("asd".to_string()),
e: Some("asd".to_string()),
}
Button {
a: "asd".to_string(),
b: "asd".to_string(),
c: "asd".to_string(),
d: Some("asd".to_string()),
e: "asd".to_string(),
}
Button {
a: "asd".to_string(),
c: "asd".to_string(),
d: Some("asd".to_string()),
}
})
}
+1
View File
@@ -2,6 +2,7 @@
name = "query_segments_demo"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+5
View File
@@ -53,6 +53,7 @@ fn App(cx: Scope) -> Element {
let formatting = "formatting!";
let formatting_tuple = ("a", "b");
let lazy_fmt = format_args!("lazily formatted text");
let asd = 123;
cx.render(rsx! {
div {
// Elements
@@ -80,6 +81,10 @@ fn App(cx: Scope) -> Element {
// pass simple rust expressions in
class: lazy_fmt,
id: format_args!("attributes can be passed lazily with std::fmt::Arguments"),
class: "asd",
class: "{asd}",
// if statements can be used to conditionally render attributes
class: if formatting.contains("form") { "{asd}" },
div {
class: {
const WORD: &str = "expressions";
+1 -1
View File
@@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
dioxus-web = { path = "../../packages/web" }
dioxus-web = { path = "../../packages/web" }
+5 -1
View File
@@ -14,9 +14,13 @@ fn main() {
}
pub fn app(cx: Scope) -> Element {
let grey_background = true;
cx.render(rsx!(
div {
header { class: "text-gray-400 bg-gray-900 body-font",
header {
class: "text-gray-400 body-font",
// you can use optional attributes to optionally apply a tailwind class
class: if grey_background { "bg-gray-900" },
div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center",
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
StacksIcon {}
+48 -41
View File
@@ -48,11 +48,8 @@ pub fn app(cx: Scope<()>) -> Element {
cx.render(rsx! {
section { class: "todoapp",
style { include_str!("./assets/todomvc.css") }
TodoHeader {
todos: todos,
}
section {
class: "main",
TodoHeader { todos: todos }
section { class: "main",
if !todos.is_empty() {
rsx! {
input {
@@ -103,31 +100,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
cx.render(rsx! {
header { class: "header",
h1 {"todos"}
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| {
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
cx.props.todos.make_mut().insert(
**todo_id,
TodoItem {
id: **todo_id,
checked: false,
contents: draft.to_string(),
},
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
h1 { "todos" }
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| {
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
cx.props
.todos
.make_mut()
.insert(
**todo_id,
TodoItem {
id: **todo_id,
checked: false,
contents: draft.to_string(),
},
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
}
}
}
}
}
})
}
@@ -146,8 +146,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
let editing = if **is_editing { "editing" } else { "" };
cx.render(rsx!{
li {
class: "{completed} {editing}",
li { class: "{completed} {editing}",
div { class: "view",
input {
class: "toggle",
@@ -160,14 +159,16 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
}
label {
r#for: "cbg-{todo.id}",
ondblclick: move |_| is_editing.set(true),
ondoubleclick: move |_| is_editing.set(true),
prevent_default: "onclick",
"{todo.contents}"
}
button {
class: "destroy",
onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
prevent_default: "onclick",
onclick: move |_| {
cx.props.todos.make_mut().remove(&todo.id);
},
prevent_default: "onclick"
}
}
is_editing.then(|| rsx!{
@@ -213,15 +214,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
cx.render(rsx! {
footer { class: "footer",
span { class: "todo-count",
strong {"{active_todo_count} "}
span {"{active_todo_text} left"}
strong { "{active_todo_count} " }
span { "{active_todo_text} left" }
}
ul { class: "filters",
for (state, state_text, url) in [
(FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"),
] {
for (state , state_text , url) in [
(FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"),
] {
li {
a {
href: url,
@@ -250,8 +251,14 @@ pub fn PageFooter(cx: Scope) -> Element {
cx.render(rsx! {
footer { class: "info",
p { "Double-click to edit a todo" }
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
p {
"Created by "
a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
}
p {
"Part of "
a { href: "http://todomvc.com", "TodoMVC" }
}
}
})
}
+184
View File
@@ -0,0 +1,184 @@
use dioxus::prelude::*;
use dioxus_desktop::wry::http;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
use std::borrow::Cow;
use std::{io::SeekFrom, path::PathBuf};
use tokio::io::AsyncReadExt;
use tokio::io::AsyncSeekExt;
use tokio::io::AsyncWriteExt;
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
fn main() {
let video_file = PathBuf::from(VIDEO_PATH);
if !video_file.exists() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
println!("Downloading video file...");
let video_url =
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
let mut response = reqwest::get(video_url).await.unwrap();
let mut file = tokio::fs::File::create(&video_file).await.unwrap();
while let Some(chunk) = response.chunk().await.unwrap() {
file.write_all(&chunk).await.unwrap();
}
});
}
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, move |request: &AssetRequest| {
let request = request.clone();
async move {
let video_file = PathBuf::from(VIDEO_PATH);
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
let response: Option<Response<Cow<'static, [u8]>>> =
match get_stream_response(&mut file, &request).await {
Ok(response) => Some(response.map(Cow::Owned)),
Err(err) => {
eprintln!("Error: {}", err);
None
}
};
response
}
});
render! {
div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
}
}
async fn get_stream_response(
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
request: &AssetRequest,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
// get stream length
let len = {
let old_pos = asset.stream_position().await?;
let len = asset.seek(SeekFrom::End(0)).await?;
asset.seek(SeekFrom::Start(old_pos)).await?;
len
};
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
// if the webview sent a range header, we need to send a 206 in return
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
let http_response = if let Some(range_header) = request.headers().get("range") {
let not_satisfiable = || {
ResponseBuilder::new()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(CONTENT_RANGE, format!("bytes */{len}"))
.body(vec![])
};
// parse range header
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
ranges
.iter()
// map the output back to spec range <start-end>, example: 0-499
.map(|r| (r.start, r.start + r.length - 1))
.collect::<Vec<_>>()
} else {
return Ok(not_satisfiable()?);
};
/// The Maximum bytes we send in one range
const MAX_LEN: u64 = 1000 * 1024;
if ranges.len() == 1 {
let &(start, mut end) = ranges.first().unwrap();
// check if a range is not satisfiable
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
return Ok(not_satisfiable()?);
}
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
// allocate a buf with a suitable capacity
let mut buf = Vec::with_capacity(bytes_to_read as usize);
// seek the file to the starting byte
asset.seek(SeekFrom::Start(start)).await?;
// read the needed bytes
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
resp = resp.status(StatusCode::PARTIAL_CONTENT);
resp.body(buf)
} else {
let mut buf = Vec::new();
let ranges = ranges
.iter()
.filter_map(|&(start, mut end)| {
// filter out unsatisfiable ranges
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
None
} else {
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
Some((start, end))
}
})
.collect::<Vec<_>>();
let boundary = format!("{:x}", rand::random::<u64>());
let boundary_sep = format!("\r\n--{boundary}\r\n");
let boundary_closer = format!("\r\n--{boundary}\r\n");
resp = resp.header(
CONTENT_TYPE,
format!("multipart/byteranges; boundary={boundary}"),
);
for (end, start) in ranges {
// a new range is being written, write the range boundary
buf.write_all(boundary_sep.as_bytes()).await?;
// write the needed headers `Content-Type` and `Content-Range`
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
.await?;
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
.await?;
// write the separator to indicate the start of the range body
buf.write_all("\r\n".as_bytes()).await?;
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
let mut local_buf = vec![0_u8; bytes_to_read as usize];
asset.seek(SeekFrom::Start(start)).await?;
asset.read_exact(&mut local_buf).await?;
buf.extend_from_slice(&local_buf);
}
// all ranges have been written, write the closing boundary
buf.write_all(boundary_closer.as_bytes()).await?;
resp.body(buf)
}
} else {
resp = resp.header(CONTENT_LENGTH, len);
let mut buf = Vec::with_capacity(len as usize);
asset.read_to_end(&mut buf).await?;
resp.body(buf)
};
http_response.map_err(Into::into)
}