Initial commit
This commit is contained in:
commit
20a9799b2c
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
2914
Cargo.lock
generated
Normal file
2914
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "film-manager"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.25", features = ["serde"] }
|
||||
eframe = { version = "0.22.0", features = ["ron", "persistence"] }
|
||||
egui-datepicker = { version = "0.3.0", path = "egui-datepicker" }
|
||||
egui_extras = { version = "0.22.0", features = ["datepicker"] }
|
||||
env_logger = "0.10.0"
|
||||
itertools = "0.10.5"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
toml = "0.7.4"
|
3
egui-datepicker/.gitignore
vendored
Normal file
3
egui-datepicker/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
.pre-commit-config.yaml
|
23
egui-datepicker/Cargo.toml
Normal file
23
egui-datepicker/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "egui-datepicker"
|
||||
version = "0.3.0"
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Adds date picker widget for egui gui library"
|
||||
keywords = ["gui"]
|
||||
categories = ["gui"]
|
||||
repository = "https://github.com/Shadr/egui-datepicker"
|
||||
include = [
|
||||
"**/*.rs",
|
||||
"Cargo.toml",
|
||||
]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.22"
|
||||
chrono = "0.4"
|
||||
num-traits = "0.2"
|
||||
|
||||
[[example]]
|
||||
name = "simple"
|
201
egui-datepicker/LICENSE-APACHE
Normal file
201
egui-datepicker/LICENSE-APACHE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
21
egui-datepicker/LICENSE-MIT
Normal file
21
egui-datepicker/LICENSE-MIT
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Shadr
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
61
egui-datepicker/README.md
Normal file
61
egui-datepicker/README.md
Normal file
@ -0,0 +1,61 @@
|
||||
# egui-datepicker
|
||||
[![Latest version](https://img.shields.io/crates/v/egui-datepicker)](https://crates.io/crates/egui-datepicker)
|
||||
|
||||
This library provide a simple date picker widget for egui with some customization. Checkout the [gif](media/preview.gif) to see widget in action!
|
||||
|
||||
<p align="center">
|
||||
<img src="media/datepicker-image.png">
|
||||
</p>
|
||||
|
||||
## ⚡️ Quickstart
|
||||
|
||||
Add `egui-datepicker` as dependency to your project
|
||||
```toml
|
||||
[dependencies]
|
||||
egui-datepicker = "0.3"
|
||||
```
|
||||
|
||||
Import necessary structs
|
||||
```rust
|
||||
use egui_datepicker::{DatePicker, Date, Utc};
|
||||
```
|
||||
|
||||
or if you already include `chrono` in your project
|
||||
```rust
|
||||
use egui_datepicker::DatePicker;
|
||||
use chrono::{Date, offset::Utc};
|
||||
```
|
||||
|
||||
Add date field with selected time offset in app struct
|
||||
```rust
|
||||
struct MyApp {
|
||||
date: Date<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
Add widget in update function
|
||||
```rust
|
||||
fn update(/*snip*/) {
|
||||
/*snip*/
|
||||
ui.add(DatePicker::new("datepicker-unique-id", &mut self.date));
|
||||
/*snip*/
|
||||
}
|
||||
```
|
||||
|
||||
## 👀 Customization
|
||||
You can set first day of week to sunday with
|
||||
```rust
|
||||
DatePicker::new(/*snip*/).sunday_first(true)
|
||||
```
|
||||
Make popup window movable
|
||||
```rust
|
||||
DatePicker::new(/*snip*/).movable(true)
|
||||
```
|
||||
Set different date format
|
||||
```rust
|
||||
DatePicker::new(/*snip*/).date_format("%d/%m/%Y")
|
||||
```
|
||||
|
||||
## ⚠️ License
|
||||
|
||||
`egui-datepicker` is licensed under MIT OR Apache-2.0
|
67
egui-datepicker/examples/simple.rs
Normal file
67
egui-datepicker/examples/simple.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use chrono::Datelike;
|
||||
use eframe::{
|
||||
egui::{self, Color32},
|
||||
epi,
|
||||
};
|
||||
use egui_datepicker::*;
|
||||
|
||||
struct ExampleApp {
|
||||
date: Date<Utc>,
|
||||
}
|
||||
|
||||
impl Default for ExampleApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
date: Utc::now().date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl epi::App for ExampleApp {
|
||||
fn name(&self) -> &str {
|
||||
"Datepicker example"
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) {
|
||||
// ctx.set_debug_on_hover(true);
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::Grid::new("exaamples_grid").show(ui, |ui| {
|
||||
ui.label("Default");
|
||||
ui.add(DatePicker::new("default", &mut self.date));
|
||||
ui.end_row();
|
||||
ui.label("Sunday first");
|
||||
ui.add(DatePicker::new("sundayfirst", &mut self.date).sunday_first(true));
|
||||
ui.end_row();
|
||||
ui.label("Movable popup");
|
||||
ui.add(DatePicker::new("movable", &mut self.date).movable(true));
|
||||
ui.end_row();
|
||||
ui.label("Different format");
|
||||
ui.add(DatePicker::new("differentformat", &mut self.date).date_format(&"%d/%m/%Y"));
|
||||
ui.end_row();
|
||||
ui.label("Disable weekend highlight");
|
||||
ui.add(
|
||||
DatePicker::new("noweekendhighlight", &mut self.date).highlight_weekend(false),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.label("Different weekend color");
|
||||
ui.add(
|
||||
DatePicker::new("differentweekendcolor", &mut self.date)
|
||||
.highlight_weekend_color(Color32::from_rgb(0, 196, 0)),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.label("Different weekend days, i.e. holidays, Christmas, etc");
|
||||
ui.add(
|
||||
DatePicker::new("differentweekenddays", &mut self.date)
|
||||
.weekend_days(|date| date.day() % 2 == 0),
|
||||
);
|
||||
ui.end_row();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let app = ExampleApp::default();
|
||||
let native_options = eframe::NativeOptions::default();
|
||||
eframe::run_native(Box::new(app), native_options);
|
||||
}
|
BIN
egui-datepicker/media/datepicker-image.png
Normal file
BIN
egui-datepicker/media/datepicker-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
BIN
egui-datepicker/media/preview.gif
Normal file
BIN
egui-datepicker/media/preview.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 171 KiB |
289
egui-datepicker/src/lib.rs
Normal file
289
egui-datepicker/src/lib.rs
Normal file
@ -0,0 +1,289 @@
|
||||
//! egui-datepicker adds a simple date picker widget.
|
||||
//! Checkout the [example][ex]
|
||||
//!
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use eframe::egui::Ui;
|
||||
//! use chrono::prelude::*;
|
||||
//! use std::fmt::Display;
|
||||
//! use egui_datepicker::DatePicker;
|
||||
//!
|
||||
//! struct App<Tz>
|
||||
//! where
|
||||
//! Tz: TimeZone,
|
||||
//! Tz::Offset: Display,
|
||||
//! {
|
||||
//! date: chrono::NaiveDate
|
||||
//! }
|
||||
//! impl<Tz> App<Tz>
|
||||
//! where
|
||||
//! Tz: TimeZone,
|
||||
//! Tz::Offset: Display,
|
||||
//! {
|
||||
//! fn draw_datepicker(&mut self, ui: &mut Ui) {
|
||||
//! ui.add(DatePicker::new("super_unique_id", &mut self.date));
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [ex]: ./examples/simple.rs
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
pub use chrono::{NaiveDate, Local};
|
||||
use chrono::{prelude::*, Duration};
|
||||
use eframe::{
|
||||
egui,
|
||||
egui::{Area, Color32, DragValue, Frame, Id, Key, Order, Response, RichText, Ui, Widget},
|
||||
};
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
/// Default values of fields are:
|
||||
/// - sunday_first: `false`
|
||||
/// - movable: `false`
|
||||
/// - format_string: `"%Y-%m-%d"`
|
||||
/// - weekend_func: `date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun`
|
||||
pub struct DatePicker<'a>
|
||||
{
|
||||
id: Id,
|
||||
date: &'a mut NaiveDate,
|
||||
sunday_first: bool,
|
||||
movable: bool,
|
||||
format_string: String,
|
||||
weekend_color: Color32,
|
||||
weekend_func: fn(&NaiveDate) -> bool,
|
||||
highlight_weekend: bool,
|
||||
}
|
||||
|
||||
impl<'a> DatePicker<'a>
|
||||
{
|
||||
/// Create new date picker with unique id and mutable reference to date.
|
||||
pub fn new<T: Hash>(id: T, date: &'a mut NaiveDate) -> Self {
|
||||
Self {
|
||||
id: Id::new(id),
|
||||
date,
|
||||
sunday_first: false,
|
||||
movable: false,
|
||||
format_string: String::from("%Y-%m-%d"),
|
||||
weekend_color: Color32::from_rgb(196, 0, 0),
|
||||
weekend_func: |date| date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun,
|
||||
highlight_weekend: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// If flag is set to true then first day in calendar will be sunday otherwise monday.
|
||||
/// Default is false
|
||||
#[must_use]
|
||||
pub fn sunday_first(mut self, flag: bool) -> Self {
|
||||
self.sunday_first = flag;
|
||||
self
|
||||
}
|
||||
|
||||
/// If flag is set to true then date picker popup will be movable.
|
||||
/// Default is false
|
||||
#[must_use]
|
||||
pub fn movable(mut self, flag: bool) -> Self {
|
||||
self.movable = flag;
|
||||
self
|
||||
}
|
||||
|
||||
///Set date format.
|
||||
///See the [chrono::format::strftime](https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html) for the specification.
|
||||
#[must_use]
|
||||
pub fn date_format(mut self, new_format: &impl ToString) -> Self {
|
||||
self.format_string = new_format.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
///If highlight is true then weekends text color will be `weekend_color` instead default text
|
||||
///color.
|
||||
#[must_use]
|
||||
pub fn highlight_weekend(mut self, highlight: bool) -> Self {
|
||||
self.highlight_weekend = highlight;
|
||||
self
|
||||
}
|
||||
|
||||
///Set weekends highlighting color.
|
||||
#[must_use]
|
||||
pub fn highlight_weekend_color(mut self, color: Color32) -> Self {
|
||||
self.weekend_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set function, which will decide if date is a weekend day or not.
|
||||
pub fn weekend_days(mut self, is_weekend: fn(&NaiveDate) -> bool) -> Self {
|
||||
self.weekend_func = is_weekend;
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw names of week days as 7 columns of grid without calling `Ui::end_row`
|
||||
fn show_grid_header(&mut self, ui: &mut Ui) {
|
||||
let day_indexes = if self.sunday_first {
|
||||
[6, 0, 1, 2, 3, 4, 5]
|
||||
} else {
|
||||
[0, 1, 2, 3, 4, 5, 6]
|
||||
};
|
||||
for i in day_indexes {
|
||||
let b = Weekday::from_u8(i).unwrap();
|
||||
ui.label(b.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Get number of days between first day of the month and Monday ( or Sunday if field
|
||||
/// `sunday_first` is set to `true` )
|
||||
fn get_start_offset_of_calendar(&self, first_day: &NaiveDate) -> u32 {
|
||||
if self.sunday_first {
|
||||
first_day.weekday().num_days_from_sunday()
|
||||
} else {
|
||||
first_day.weekday().num_days_from_monday()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get number of days between first day of the next month and Monday ( or Sunday if field
|
||||
/// `sunday_first` is set to `true` )
|
||||
fn get_end_offset_of_calendar(&self, first_day: &NaiveDate) -> u32 {
|
||||
if self.sunday_first {
|
||||
(7 - (first_day).weekday().num_days_from_sunday()) % 7
|
||||
} else {
|
||||
(7 - (first_day).weekday().num_days_from_monday()) % 7
|
||||
}
|
||||
}
|
||||
|
||||
fn show_calendar_grid(&mut self, ui: &mut Ui) {
|
||||
egui::Grid::new("calendar").show(ui, |ui| {
|
||||
self.show_grid_header(ui);
|
||||
let first_day_of_current_month = self.date.with_day(1).unwrap();
|
||||
let start_offset = self.get_start_offset_of_calendar(&first_day_of_current_month);
|
||||
let days_in_month = get_days_from_month(self.date.year(), self.date.month());
|
||||
let first_day_of_next_month =
|
||||
first_day_of_current_month.clone() + Duration::days(days_in_month);
|
||||
let end_offset = self.get_end_offset_of_calendar(&first_day_of_next_month);
|
||||
let start_date = first_day_of_current_month - Duration::days(start_offset.into());
|
||||
for i in 0..(start_offset as i64 + days_in_month + end_offset as i64) {
|
||||
if i % 7 == 0 {
|
||||
ui.end_row();
|
||||
}
|
||||
let d = start_date.clone() + Duration::days(i);
|
||||
self.show_day_button(d, ui);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_day_button(&mut self, date: NaiveDate, ui: &mut Ui) {
|
||||
ui.add_enabled_ui(self.date != &date, |ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
if self.date.month() != date.month() {
|
||||
ui.style_mut().visuals.button_frame = false;
|
||||
}
|
||||
if self.highlight_weekend && (self.weekend_func)(&date) {
|
||||
ui.style_mut().visuals.override_text_color = Some(self.weekend_color);
|
||||
}
|
||||
if ui.button(date.day().to_string()).clicked() {
|
||||
*self.date = date;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw current month and buttons for next and previous month.
|
||||
fn show_header(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
self.show_month_control(ui);
|
||||
self.show_year_control(ui);
|
||||
if ui.button("Today").clicked() {
|
||||
*self.date = Local::now().date_naive();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw button with text and add duration to current date when that button is clicked.
|
||||
fn date_step_button(&mut self, ui: &mut Ui, text: impl ToString, duration: Duration) {
|
||||
if ui.button(text.to_string()).clicked() {
|
||||
*self.date = self.date.clone() + duration;
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw drag value widget with current year and two buttons which substract and add 365 days
|
||||
/// to current date.
|
||||
fn show_year_control(&mut self, ui: &mut Ui) {
|
||||
self.date_step_button(ui, "<", Duration::days(-365));
|
||||
let mut drag_year = self.date.year();
|
||||
ui.add(DragValue::new(&mut drag_year));
|
||||
if drag_year != self.date.year() {
|
||||
*self.date = self.date.with_year(drag_year).unwrap();
|
||||
}
|
||||
self.date_step_button(ui, ">", Duration::days(365));
|
||||
}
|
||||
|
||||
/// Draw label(will be combobox in future) with current month and two buttons which substract and add 30 days
|
||||
/// to current date.
|
||||
fn show_month_control(&mut self, ui: &mut Ui) {
|
||||
self.date_step_button(ui, "<", Duration::days(-30));
|
||||
let month_string = chrono::Month::from_u32(self.date.month()).unwrap().name();
|
||||
// TODO: When https://github.com/emilk/egui/pull/543 is merged try to change label to combo box.
|
||||
ui.add(egui::Label::new(
|
||||
RichText::new(format!("{: <9}", month_string)).text_style(egui::TextStyle::Monospace),
|
||||
));
|
||||
// let mut selected = self.date.month0() as usize;
|
||||
// egui::ComboBox::from_id_source(self.id.with("month_combo_box"))
|
||||
// .selected_text(selected)
|
||||
// .show_index(ui, &mut selected, 12, |i| {
|
||||
// chrono::Month::from_usize(i + 1).unwrap().name().to_string()
|
||||
// });
|
||||
// if selected != self.date.month0() as usize {
|
||||
// *self.date = self.date.with_month0(selected as u32).unwrap();
|
||||
// }
|
||||
self.date_step_button(ui, ">", Duration::days(30));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for DatePicker<'a> {
|
||||
fn ui(mut self, ui: &mut Ui) -> Response {
|
||||
let formated_date = self.date.format(&self.format_string);
|
||||
let button_response = ui.button(formated_date.to_string());
|
||||
if button_response.clicked() {
|
||||
ui.memory_mut(|mem| mem.toggle_popup(self.id));
|
||||
}
|
||||
|
||||
if ui.memory(|mem| mem.is_popup_open(self.id)) {
|
||||
let mut area = Area::new(self.id)
|
||||
.order(Order::Foreground)
|
||||
.default_pos(button_response.rect.left_bottom());
|
||||
if !self.movable {
|
||||
area = area.movable(false);
|
||||
}
|
||||
let area_response = area
|
||||
.show(ui.ctx(), |ui| {
|
||||
Frame::popup(ui.style()).show(ui, |ui| {
|
||||
self.show_header(ui);
|
||||
self.show_calendar_grid(ui);
|
||||
});
|
||||
})
|
||||
.response;
|
||||
|
||||
if !button_response.clicked()
|
||||
&& (ui.input(|inp| inp.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
|
||||
{
|
||||
ui.memory_mut(|mem| mem.toggle_popup(self.id));
|
||||
}
|
||||
}
|
||||
button_response
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/58188385
|
||||
fn get_days_from_month(year: i32, month: u32) -> i64 {
|
||||
NaiveDate::from_ymd_opt(
|
||||
match month {
|
||||
12 => year + 1,
|
||||
_ => year,
|
||||
},
|
||||
match month {
|
||||
12 => 1,
|
||||
_ => month + 1,
|
||||
},
|
||||
1,
|
||||
).unwrap()
|
||||
.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
|
||||
.num_days()
|
||||
}
|
308
src/main.rs
Normal file
308
src/main.rs
Normal file
@ -0,0 +1,308 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use eframe::{
|
||||
egui::{self, RichText, Slider, Window},
|
||||
epaint::Color32,
|
||||
};
|
||||
use egui_datepicker::{DatePicker, Local, NaiveDate};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions {
|
||||
initial_window_size: Some(egui::vec2(320.0, 240.0)),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"Film Manager",
|
||||
options,
|
||||
Box::new(|cc| Box::new(MyApp::new(cc))),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Exposure {
|
||||
num: u8,
|
||||
#[serde(default)]
|
||||
printed: bool,
|
||||
#[serde(default)]
|
||||
damaged: bool,
|
||||
#[serde(default)]
|
||||
paper: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Roll {
|
||||
id: String,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
desc: String,
|
||||
date: NaiveDate,
|
||||
exposures: Vec<Exposure>,
|
||||
}
|
||||
|
||||
impl Roll {
|
||||
fn new(id: String, name: String, desc: String, date: NaiveDate, num_exposures: u8) -> Self {
|
||||
let mut exposures = Vec::new();
|
||||
for num in 1..=num_exposures {
|
||||
exposures.push(Exposure {
|
||||
num,
|
||||
printed: false,
|
||||
damaged: false,
|
||||
paper: None,
|
||||
})
|
||||
}
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
desc,
|
||||
date,
|
||||
exposures,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_str(&self) -> String {
|
||||
format!(
|
||||
"{}{} on {}, {} exp",
|
||||
self.id,
|
||||
if self.name.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" - {}", self.name)
|
||||
},
|
||||
self.date.format("%m/%d/%Y"),
|
||||
self.exposures.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
rolls: Vec<Roll>,
|
||||
}
|
||||
|
||||
struct MyApp {
|
||||
state: AppState,
|
||||
view_roll_state: Option<ViewRollState>,
|
||||
new_roll_state: Option<NewRollState>,
|
||||
}
|
||||
|
||||
struct ViewRollState {
|
||||
roll: usize,
|
||||
confirm_delete: bool,
|
||||
exp: u8,
|
||||
}
|
||||
|
||||
impl ViewRollState {
|
||||
fn new(roll: usize) -> Self {
|
||||
Self {
|
||||
roll,
|
||||
confirm_delete: false,
|
||||
exp: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NewRollState {
|
||||
id: String,
|
||||
name: String,
|
||||
desc: String,
|
||||
date: NaiveDate,
|
||||
exps: u8,
|
||||
show_id_req_err: bool,
|
||||
}
|
||||
|
||||
impl Default for NewRollState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
name: String::new(),
|
||||
desc: String::new(),
|
||||
date: Local::now().date_naive(),
|
||||
exps: 24,
|
||||
show_id_req_err: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MyApp {
|
||||
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let rolls = eframe::get_value(cc.storage.unwrap(), "rolls").unwrap_or_else(|| {
|
||||
vec![Roll::new(
|
||||
"00001".into(),
|
||||
"Test".into(),
|
||||
"Testing".into(),
|
||||
Local::now().date_naive(),
|
||||
24,
|
||||
)]
|
||||
});
|
||||
Self {
|
||||
state: AppState { rolls },
|
||||
view_roll_state: None,
|
||||
new_roll_state: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
eframe::set_value(storage, "rolls", &self.state.rolls);
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("Film Manager");
|
||||
ui.label("Rolls:");
|
||||
for (i, roll) in self.state.rolls.iter().enumerate() {
|
||||
if ui.button(roll.short_str()).clicked() && self.view_roll_state.is_none() {
|
||||
self.view_roll_state = Some(ViewRollState::new(i));
|
||||
}
|
||||
}
|
||||
if ui.button("Add").clicked() && self.new_roll_state.is_none() {
|
||||
self.new_roll_state = Some(NewRollState::default());
|
||||
}
|
||||
if let Some(view_roll_state) = self.view_roll_state.as_mut() {
|
||||
if view_roll_window(ctx, view_roll_state, &mut self.state) {
|
||||
self.view_roll_state = None;
|
||||
}
|
||||
}
|
||||
if let Some(new_roll_state) = self.new_roll_state.as_mut() {
|
||||
if new_roll_window(ctx, new_roll_state, &mut self.state) {
|
||||
self.new_roll_state = None;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn view_roll_window(
|
||||
ctx: &egui::Context,
|
||||
win_state: &mut ViewRollState,
|
||||
app_state: &mut AppState,
|
||||
) -> bool {
|
||||
let mut open = true;
|
||||
let window =
|
||||
Window::new(format!("Roll {}", app_state.rolls[win_state.roll].id)).open(&mut open);
|
||||
let deleted = window
|
||||
.show(ctx, |ui| {
|
||||
let roll = &mut app_state.rolls[win_state.roll];
|
||||
ui.label(format!("Name: {}", roll.name));
|
||||
ui.label(format!("Description: {}", roll.desc));
|
||||
ui.label(format!("Date: {}", roll.date.format("%Y-%m-%d")));
|
||||
ui.label("Exposures:");
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
for (i, chunk) in roll.exposures.iter().chunks(5).into_iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
for (j, exp) in chunk.enumerate() {
|
||||
let num = i * 5 + j;
|
||||
let mut label = RichText::new(format!("{:02}", exp.num));
|
||||
if exp.printed {
|
||||
if exp.damaged {
|
||||
label = label.color(Color32::DARK_RED);
|
||||
} else {
|
||||
label = label.color(Color32::DARK_BLUE);
|
||||
}
|
||||
}
|
||||
ui.selectable_value(&mut win_state.exp, num as u8, label);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
let exp = &mut roll.exposures[win_state.exp as usize];
|
||||
ui.checkbox(&mut exp.printed, "Printed");
|
||||
if !exp.printed {
|
||||
exp.damaged = false;
|
||||
}
|
||||
if exp.printed {
|
||||
ui.checkbox(&mut exp.damaged, "Damaged");
|
||||
}
|
||||
});
|
||||
});
|
||||
if ui.button("Delete").clicked() {
|
||||
win_state.confirm_delete = true;
|
||||
}
|
||||
if win_state.confirm_delete {
|
||||
Window::new("Confirm delete")
|
||||
.show(ctx, |ui| {
|
||||
ui.heading(format!(
|
||||
"Really delete roll {}?",
|
||||
app_state.rolls[win_state.roll].id
|
||||
));
|
||||
if ui.button("No").clicked() {
|
||||
win_state.confirm_delete = false;
|
||||
return false;
|
||||
}
|
||||
if ui.button("Yes").clicked() {
|
||||
app_state.rolls.remove(win_state.roll);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
})
|
||||
.unwrap()
|
||||
.inner
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.inner
|
||||
.unwrap_or(false);
|
||||
!open || deleted
|
||||
}
|
||||
|
||||
fn new_roll_window(
|
||||
ctx: &egui::Context,
|
||||
win_state: &mut NewRollState,
|
||||
app_state: &mut AppState,
|
||||
) -> bool {
|
||||
let mut open = true;
|
||||
Window::new("New Roll").open(&mut open).show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label("ID: ");
|
||||
ui.text_edit_singleline(&mut win_state.id)
|
||||
.labelled_by(label.id);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label("Name: ");
|
||||
ui.text_edit_singleline(&mut win_state.name)
|
||||
.labelled_by(label.id);
|
||||
});
|
||||
let desc_label = ui.label("Description: ");
|
||||
ui.text_edit_multiline(&mut win_state.desc)
|
||||
.labelled_by(desc_label.id);
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label("Date: ");
|
||||
ui.add(
|
||||
DatePicker::new("new_roll_date", &mut win_state.date)
|
||||
.sunday_first(true)
|
||||
.highlight_weekend(false),
|
||||
)
|
||||
.labelled_by(label.id);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label("Num exposures: ");
|
||||
ui.add(Slider::new(&mut win_state.exps, 1..=50).step_by(1.0))
|
||||
.labelled_by(label.id);
|
||||
});
|
||||
if ui.button("Submit").clicked() {
|
||||
if win_state.id.is_empty() {
|
||||
win_state.show_id_req_err = true;
|
||||
} else {
|
||||
app_state.rolls.push(Roll::new(
|
||||
win_state.id.clone(),
|
||||
win_state.name.clone(),
|
||||
win_state.desc.clone(),
|
||||
win_state.date,
|
||||
win_state.exps,
|
||||
));
|
||||
}
|
||||
}
|
||||
if win_state.show_id_req_err {
|
||||
ui.colored_label(Color32::DARK_RED, "Error: ID required");
|
||||
}
|
||||
});
|
||||
!open
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user