├── .gitignore ├── Cargo.toml ├── LICENSE-MIT ├── src ├── week_day.rs ├── l16n.rs ├── month.rs └── lib.rs ├── examples ├── double.rs └── basic.rs ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cursive_calendar_view" 3 | version = "0.9.0" 4 | authors = ["Ivo Wetzel ", "Alexandre Bury "] 5 | description = "A basic calendar view implementation for cursive." 6 | repository = "https://github.com/BonsaiDen/cursive_calendar_view.git" 7 | readme = "README.md" 8 | keywords = ["cursive", "ncurses", "TUI", "UI", "calendar"] 9 | categories = ["command-line-interface", "gui"] 10 | license = "MIT/Apache-2.0" 11 | edition = "2018" 12 | 13 | [dependencies] 14 | cursive_core = "0.4" 15 | chrono = "0.4" 16 | 17 | [dev-dependencies] 18 | rand = "0.8" 19 | lazy_static = "1.2" 20 | cursive = "0.21" 21 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017 Ivo Wetzel 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /src/week_day.rs: -------------------------------------------------------------------------------- 1 | /// Enumeration of all weekdays. 2 | #[derive(Copy, Clone)] 3 | pub enum WeekDay { 4 | /// Monday. 5 | Monday, 6 | /// Tuesday. 7 | Tuesday, 8 | /// Wednesday. 9 | Wednesday, 10 | /// Thursday. 11 | Thursday, 12 | /// Friday. 13 | Friday, 14 | /// Saturday. 15 | Saturday, 16 | /// Sunday. 17 | Sunday, 18 | } 19 | 20 | // Statics -------------------------------------------------------------------- 21 | static WEEK_DAY_LIST: [WeekDay; 7] = [ 22 | WeekDay::Monday, 23 | WeekDay::Tuesday, 24 | WeekDay::Wednesday, 25 | WeekDay::Thursday, 26 | WeekDay::Friday, 27 | WeekDay::Saturday, 28 | WeekDay::Sunday, 29 | ]; 30 | 31 | // Conversions ---------------------------------------------------------------- 32 | impl From for WeekDay { 33 | fn from(index: i32) -> Self { 34 | WEEK_DAY_LIST[((index + 7) % 7) as usize] 35 | } 36 | } 37 | 38 | impl Into for WeekDay { 39 | fn into(self) -> i32 { 40 | match self { 41 | WeekDay::Monday => 0, 42 | WeekDay::Tuesday => 1, 43 | WeekDay::Wednesday => 2, 44 | WeekDay::Thursday => 3, 45 | WeekDay::Friday => 4, 46 | WeekDay::Saturday => 5, 47 | WeekDay::Sunday => 6, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/double.rs: -------------------------------------------------------------------------------- 1 | // Crate Dependencies --------------------------------------------------------- 2 | 3 | use cursive; 4 | 5 | 6 | // External Dependencies ------------------------------------------------------ 7 | use chrono::prelude::*; 8 | use cursive::direction::Orientation; 9 | use cursive::views::{Dialog, DummyView, LinearLayout, ResizedView}; 10 | 11 | // Modules -------------------------------------------------------------------- 12 | use cursive_calendar_view::{CalendarView, EnglishLocale, ViewMode}; 13 | 14 | // Example -------------------------------------------------------------------- 15 | fn main() { 16 | let mut siv = cursive::default(); 17 | 18 | let mut calendar_a = CalendarView::::new(Utc.ymd(2017, 7, 26)); 19 | calendar_a.set_highest_view_mode(ViewMode::Year); 20 | calendar_a.set_earliest_date(Some(Utc.ymd(2017, 1, 1))); 21 | calendar_a.set_latest_date(Some(Utc.ymd(2017, 12, 31))); 22 | calendar_a.set_show_iso_weeks(true); 23 | 24 | let mut calendar_b = CalendarView::::new(Utc.ymd(2017, 7, 26)); 25 | calendar_b.set_highest_view_mode(ViewMode::Year); 26 | calendar_b.set_earliest_date(Some(Utc.ymd(2017, 1, 1))); 27 | calendar_b.set_latest_date(Some(Utc.ymd(2017, 12, 31))); 28 | 29 | let mut layout = LinearLayout::new(Orientation::Horizontal); 30 | layout.add_child(calendar_a); 31 | layout.add_child(ResizedView::with_fixed_size((4, 0), DummyView)); 32 | layout.add_child(calendar_b); 33 | 34 | siv.add_layer(Dialog::around(layout).title("Calendar View Demo")); 35 | 36 | siv.run(); 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cursive-calendar-view 2 | 3 | [![cursive-calendar-view on crates.io][cratesio-image]][cratesio] 4 | [![cursive-calendar-view on docs.rs][docsrs-image]][docsrs] 5 | 6 | [cratesio-image]: https://img.shields.io/crates/v/cursive_calendar_view.svg 7 | [cratesio]: https://crates.io/crates/cursive_calendar_view 8 | [docsrs-image]: https://docs.rs/cursive_calendar_view/badge.svg 9 | [docsrs]: https://docs.rs/cursive_calendar_view/ 10 | 11 | A basic calendar view implementation for [cursive](https://crates.io/crates/cursive). 12 | 13 | ![Month View](https://cloud.githubusercontent.com/assets/124674/25067601/b4f39c9e-2248-11e7-8bea-5d1a1c7669ac.png) ![Year View](https://cloud.githubusercontent.com/assets/124674/25067602/b6233084-2248-11e7-81e1-c7874b2c3d7c.png) ![Decade View](https://cloud.githubusercontent.com/assets/124674/25067604/c10f8bc8-2248-11e7-9c1c-f8a1b9fdff8c.png) 14 | 15 | ## Usage 16 | 17 | Add this to your `Cargo.toml`: 18 | 19 | ```toml 20 | [dependencies] 21 | cursive_calendar_view = "0.8" 22 | ``` 23 | 24 | and this to your crate root: 25 | 26 | ```rust 27 | extern crate cursive_calendar_view; 28 | ``` 29 | 30 | ## License 31 | 32 | Licensed under either of 33 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 34 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 35 | at your option. 36 | 37 | 38 | ### Contribution 39 | 40 | Unless you explicitly state otherwise, any contribution intentionally submitted 41 | for inclusion in the work by you shall be dual licensed as above, without any 42 | additional terms or conditions. 43 | 44 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | // Crate Dependencies --------------------------------------------------------- 2 | 3 | use cursive; 4 | 5 | // STD Dependencies ----------------------------------------------------------- 6 | use std::sync::{Arc, Mutex}; 7 | 8 | // External Dependencies ------------------------------------------------------ 9 | use chrono::prelude::*; 10 | use cursive::traits::*; 11 | use cursive::views::{Dialog, TextView}; 12 | use cursive::Cursive; 13 | 14 | // Modules -------------------------------------------------------------------- 15 | use cursive_calendar_view::{CalendarView, EnglishLocale, ViewMode}; 16 | 17 | // Example -------------------------------------------------------------------- 18 | fn main() { 19 | let mut siv = cursive::default(); 20 | 21 | let stored_date: Arc>> = Arc::new(Mutex::new(Utc.ymd(2020, 12, 31))); 22 | siv.add_layer( 23 | Dialog::around(TextView::new("-").with_name("text_box")) 24 | .button("Choose Date...", move |s| { 25 | let mut calendar = 26 | CalendarView::::new(*stored_date.lock().unwrap()); 27 | 28 | //calendar.set_highest_view_mode(ViewMode::Year); 29 | calendar.set_view_mode(ViewMode::Year); 30 | calendar.set_earliest_date(Some(Utc.ymd(2020, 1, 1))); 31 | calendar.set_latest_date(Some(Utc.ymd(2040, 12, 31))); 32 | calendar.set_show_iso_weeks(true); 33 | 34 | let inner_date = stored_date.clone(); 35 | calendar.set_on_submit(move |siv: &mut Cursive, date: &Date| { 36 | siv.call_on_name("text_box", |view: &mut TextView| { 37 | *inner_date.lock().unwrap() = *date; 38 | view.set_content(format!("{}", date)); 39 | }); 40 | siv.pop_layer(); 41 | }); 42 | 43 | s.add_layer(Dialog::around(calendar.with_name("calendar")).title("Select Date")); 44 | }) 45 | .title("Calendar View Demo"), 46 | ); 47 | 48 | siv.run(); 49 | } 50 | -------------------------------------------------------------------------------- /src/l16n.rs: -------------------------------------------------------------------------------- 1 | // Internal Dependencies ------------------------------------------------------ 2 | use crate::{Month, WeekDay}; 3 | 4 | /// Trait for localization of a [`CalendarView`](struct.CalendarView.html). 5 | pub trait Locale { 6 | /// Method returning the localized string for a specific [`WeekDay`](enum.WeekDay.html). 7 | /// 8 | /// Both *short* e.g. `Th` and *long* translations e.g. `Thursday` are suppported. 9 | fn week_day(day: WeekDay, long_text: bool) -> &'static str; 10 | 11 | /// Method returning the localized string for a specific [`Month`](enum.Month.html). 12 | /// 13 | /// Both *short* e.g. `Dec` and *long* translations e.g. `December` are suppported. 14 | fn month(month: Month, long_text: bool) -> &'static str; 15 | } 16 | 17 | /// English locale for a [`CalendarView`](struct.CalendarView.html). 18 | pub struct EnglishLocale; 19 | 20 | impl Locale for EnglishLocale { 21 | fn week_day(day: WeekDay, long_text: bool) -> &'static str { 22 | if long_text { 23 | match day { 24 | WeekDay::Monday => "Monday", 25 | WeekDay::Tuesday => "Tuesday", 26 | WeekDay::Wednesday => "Wednesday", 27 | WeekDay::Thursday => "Thursday", 28 | WeekDay::Friday => "Friday", 29 | WeekDay::Saturday => "Saturday", 30 | WeekDay::Sunday => "Sunday", 31 | } 32 | } else { 33 | match day { 34 | WeekDay::Monday => "Mo", 35 | WeekDay::Tuesday => "Tu", 36 | WeekDay::Wednesday => "We", 37 | WeekDay::Thursday => "Th", 38 | WeekDay::Friday => "Fr", 39 | WeekDay::Saturday => "Sa", 40 | WeekDay::Sunday => "Su", 41 | } 42 | } 43 | } 44 | 45 | fn month(month: Month, long_text: bool) -> &'static str { 46 | if long_text { 47 | match month { 48 | Month::January => "January", 49 | Month::February => "February", 50 | Month::March => "March", 51 | Month::April => "April", 52 | Month::May => "May", 53 | Month::June => "June", 54 | Month::July => "July", 55 | Month::August => "August", 56 | Month::September => "September", 57 | Month::October => "October", 58 | Month::November => "November", 59 | Month::December => "December", 60 | } 61 | } else { 62 | match month { 63 | Month::January => "Jan", 64 | Month::February => "Feb", 65 | Month::March => "Mar", 66 | Month::April => "Apr", 67 | Month::May => "May", 68 | Month::June => "Jun", 69 | Month::July => "Jul", 70 | Month::August => "Aug", 71 | Month::September => "Sep", 72 | Month::October => "Oct", 73 | Month::November => "Nov", 74 | Month::December => "Dec", 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/month.rs: -------------------------------------------------------------------------------- 1 | /// Enumeration of all months in a year. 2 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 3 | pub enum Month { 4 | /// The month of January. 5 | January, 6 | /// The month of February. 7 | February, 8 | /// The month of March. 9 | March, 10 | /// The month of April. 11 | April, 12 | /// The month of May. 13 | May, 14 | /// The month of June. 15 | June, 16 | /// The month of July. 17 | July, 18 | /// The month of August. 19 | August, 20 | /// The month of September. 21 | September, 22 | /// The month of October. 23 | October, 24 | /// The month of November. 25 | November, 26 | /// The month of December. 27 | December, 28 | } 29 | 30 | impl Month { 31 | #[doc(hidden)] 32 | pub fn prev(self) -> Self { 33 | let index: i32 = self.into(); 34 | MONTH_LIST[(((index - 1) + 12) % 12) as usize] 35 | } 36 | 37 | #[doc(hidden)] 38 | pub fn number_of_days(self, year: i32) -> i32 { 39 | match self { 40 | Month::February => { 41 | if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 { 42 | 29 43 | } else { 44 | 28 45 | } 46 | } 47 | Month::January 48 | | Month::March 49 | | Month::May 50 | | Month::July 51 | | Month::August 52 | | Month::October 53 | | Month::December => 31, 54 | Month::April | Month::June | Month::September | Month::November => 30, 55 | } 56 | } 57 | 58 | #[doc(hidden)] 59 | pub fn prev_number_of_days(self, year: i32) -> i32 { 60 | match self { 61 | Month::January => self.prev().number_of_days(year - 1), 62 | _ => self.prev().number_of_days(year), 63 | } 64 | } 65 | } 66 | 67 | // Statics -------------------------------------------------------------------- 68 | static MONTH_LIST: [Month; 12] = [ 69 | Month::January, 70 | Month::February, 71 | Month::March, 72 | Month::April, 73 | Month::May, 74 | Month::June, 75 | Month::July, 76 | Month::August, 77 | Month::September, 78 | Month::October, 79 | Month::November, 80 | Month::December, 81 | ]; 82 | 83 | // Conversions ---------------------------------------------------------------- 84 | impl From for Month { 85 | fn from(index: u32) -> Self { 86 | MONTH_LIST[index as usize] 87 | } 88 | } 89 | 90 | impl<'a> Into for Month { 91 | fn into(self) -> i32 { 92 | match self { 93 | Month::January => 0, 94 | Month::February => 1, 95 | Month::March => 2, 96 | Month::April => 3, 97 | Month::May => 4, 98 | Month::June => 5, 99 | Month::July => 6, 100 | Month::August => 7, 101 | Month::September => 8, 102 | Month::October => 9, 103 | Month::November => 10, 104 | Month::December => 11, 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A calendar implementation for [cursive](https://crates.io/crates/cursive). 2 | #![deny( 3 | missing_docs, 4 | trivial_casts, 5 | trivial_numeric_casts, 6 | unsafe_code, 7 | unused_import_braces, 8 | unused_qualifications 9 | )] 10 | 11 | // Crate Dependencies --------------------------------------------------------- 12 | 13 | extern crate cursive_core as cursive; 14 | 15 | // STD Dependencies ----------------------------------------------------------- 16 | use std::cmp; 17 | use std::marker::PhantomData; 18 | use std::sync::Arc; 19 | 20 | // External Dependencies ------------------------------------------------------ 21 | use chrono::offset::TimeZone; 22 | use chrono::prelude::*; 23 | 24 | use crate::cursive::direction::Direction; 25 | use crate::cursive::event::{Callback, Event, EventResult, Key, MouseButton, MouseEvent}; 26 | use crate::cursive::theme::ColorStyle; 27 | use crate::cursive::vec::Vec2; 28 | use crate::cursive::view::{CannotFocus, View}; 29 | use crate::cursive::With; 30 | use crate::cursive::{Cursive, Printer}; 31 | 32 | // Modules -------------------------------------------------------------------- 33 | mod l16n; 34 | mod month; 35 | mod week_day; 36 | 37 | // Re-Exports ----------------------------------------------------------------- 38 | pub use crate::l16n::{EnglishLocale, Locale}; 39 | pub use crate::month::Month; 40 | pub use crate::week_day::WeekDay; 41 | 42 | /// Enumeration of all view modes supported by a [`CalendarView`](struct.CalendarView.html). 43 | #[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] 44 | pub enum ViewMode { 45 | /// View of a specific month, allowing selection of individual days. 46 | Month, 47 | /// View of a specific year, allowing selection of individual months. 48 | Year, 49 | /// View of a specific decade, allowing selection of individual years. 50 | Decade, 51 | } 52 | 53 | /// A callback taking a date as parameter. 54 | /// 55 | /// This is an internal type used to improve readability. 56 | type DateCallback = Arc) + Send + Sync>; 57 | 58 | /// View for selecting a date, supporting different modes for day, month or 59 | /// year based selection. 60 | /// 61 | /// View modes can be navigated via `Backspace` and `Enter`. 62 | /// 63 | /// Custom localization is possible by providing an implementation of the 64 | /// [`Locale`](trait.Locale.html) trait. 65 | /// 66 | /// # Examples 67 | /// 68 | /// ``` 69 | /// # extern crate cursive; 70 | /// # extern crate cursive_calendar_view; 71 | /// # extern crate chrono; 72 | /// # use chrono::prelude::*; 73 | /// # use cursive_calendar_view::{CalendarView, EnglishLocale, ViewMode}; 74 | /// # fn main() { 75 | /// // Allow selection a date within the year of 2017. 76 | /// let mut calendar = CalendarView::::new(Utc::today()); 77 | /// 78 | /// calendar.set_highest_view_mode(ViewMode::Year); 79 | /// calendar.set_earliest_date(Some(Utc.ymd(2017, 1, 1))); 80 | /// calendar.set_latest_date(Some(Utc.ymd(2017, 12, 31))); 81 | /// calendar.set_show_iso_weeks(true); 82 | /// # } 83 | /// ``` 84 | pub struct CalendarView { 85 | enabled: bool, 86 | show_iso_weeks: bool, 87 | week_start: WeekDay, 88 | 89 | highest_view_mode: ViewMode, 90 | lowest_view_mode: ViewMode, 91 | 92 | view_mode: ViewMode, 93 | view_date: Date, 94 | 95 | earliest_date: Option>, 96 | latest_date: Option>, 97 | date: Date, 98 | on_submit: Option>, 99 | on_select: Option>, 100 | 101 | size: Vec2, 102 | 103 | _localization: PhantomData, 104 | } 105 | 106 | impl CalendarView 107 | where 108 | T::Offset: Send + Sync, 109 | { 110 | /// Creates new `CalendarView`. 111 | pub fn new(today: Date) -> Self { 112 | Self { 113 | enabled: true, 114 | highest_view_mode: ViewMode::Decade, 115 | lowest_view_mode: ViewMode::Month, 116 | show_iso_weeks: false, 117 | week_start: WeekDay::Monday, 118 | date: today.clone(), 119 | earliest_date: None, 120 | latest_date: None, 121 | view_mode: ViewMode::Month, 122 | view_date: today, 123 | size: (0, 0).into(), 124 | on_submit: None, 125 | on_select: None, 126 | _localization: PhantomData, 127 | } 128 | } 129 | 130 | /// Disables this view. 131 | /// 132 | /// A disabled view cannot be selected. 133 | pub fn disable(&mut self) { 134 | self.enabled = false; 135 | } 136 | 137 | /// Re-enables this view. 138 | pub fn enable(&mut self) { 139 | self.enabled = true; 140 | } 141 | 142 | /// Enable or disable this view. 143 | pub fn set_enabled(&mut self, enabled: bool) { 144 | self.enabled = enabled; 145 | } 146 | 147 | /// Returns `true` if this view is enabled. 148 | pub fn is_enabled(&self) -> bool { 149 | self.enabled 150 | } 151 | 152 | /// Returns the currently selected date of this view. 153 | pub fn date(&self) -> Date { 154 | self.date.clone() 155 | } 156 | 157 | /// Sets the currently selected date of this view. 158 | pub fn set_selected_date(&mut self, mut date: Date) { 159 | if let Some(ref earliest) = self.earliest_date { 160 | if date < *earliest { 161 | date = earliest.clone(); 162 | } 163 | } 164 | 165 | if let Some(ref latest) = self.latest_date { 166 | if date > *latest { 167 | date = latest.clone(); 168 | } 169 | } 170 | 171 | self.date = date; 172 | } 173 | 174 | /// Sets the currently selected date of this view. 175 | /// 176 | /// Chainable variant. 177 | pub fn selected_date(self, date: Date) -> Self { 178 | self.with(|v| v.set_selected_date(date)) 179 | } 180 | 181 | /// Sets the visually selected date of this view. 182 | pub fn set_view_date(&mut self, mut date: Date) { 183 | if let Some(ref earliest) = self.earliest_date { 184 | if date < *earliest { 185 | date = earliest.clone(); 186 | } 187 | } 188 | 189 | if let Some(ref latest) = self.latest_date { 190 | if date > *latest { 191 | date = latest.clone(); 192 | } 193 | } 194 | 195 | self.view_date = date; 196 | } 197 | 198 | /// Sets the visually selected date of this view. 199 | /// 200 | /// Chainable variant. 201 | pub fn view_date(self, date: Date) -> Self { 202 | self.with(|v| v.set_view_date(date)) 203 | } 204 | 205 | /// Sets the currently active view mode of this view. 206 | pub fn set_view_mode(&mut self, mode: ViewMode) { 207 | if mode >= self.lowest_view_mode && mode <= self.highest_view_mode { 208 | self.view_mode = mode; 209 | } 210 | } 211 | 212 | /// Sets the currently active view mode of this view. 213 | /// 214 | /// Chainable variant. 215 | pub fn view_mode(self, mode: ViewMode) -> Self { 216 | self.with(|v| v.set_view_mode(mode)) 217 | } 218 | 219 | /// Sets the lowest view mode this calendar can be in. 220 | /// 221 | /// Can be used conjunction with 222 | /// [`CalendarView::set_highest_view_mode`](struct.CalendarView.html#method.set_highest_view_mode) 223 | /// to limit a `CalendarView` to only allow selection of days, months or years. 224 | pub fn set_lowest_view_mode(&mut self, mode: ViewMode) { 225 | if mode < self.highest_view_mode { 226 | self.lowest_view_mode = mode; 227 | if self.view_mode < self.lowest_view_mode { 228 | self.view_mode = self.lowest_view_mode; 229 | } 230 | } 231 | } 232 | 233 | /// Sets the lowest view mode this calendar can be in. 234 | /// 235 | /// Can be used conjunction with 236 | /// [`CalendarView::set_highest_view_mode`](struct.CalendarView.html#method.set_highest_view_mode) 237 | /// to limit a `CalendarView` to only allow selection of days, months or years. 238 | /// 239 | /// Chainable variant. 240 | pub fn lowest_view_mode(self, mode: ViewMode) -> Self { 241 | self.with(|v| v.set_lowest_view_mode(mode)) 242 | } 243 | 244 | /// Sets the highest view mode this calendar can be in. 245 | /// 246 | /// Can be used conjunction with 247 | /// [`CalendarView::set_lowest_view_mode`](struct.CalendarView.html#method.set_lowest_view_mode) 248 | /// to limit a `CalendarView` to only allow selection of days, months or years. 249 | pub fn set_highest_view_mode(&mut self, mode: ViewMode) { 250 | if mode > self.lowest_view_mode { 251 | self.highest_view_mode = mode; 252 | if self.view_mode > self.highest_view_mode { 253 | self.view_mode = self.highest_view_mode; 254 | } 255 | } 256 | } 257 | 258 | /// Sets the highest view mode this calendar can be in. 259 | /// 260 | /// Can be used conjunction with 261 | /// [`CalendarView::set_lowest_view_mode`](struct.CalendarView.html#method.set_lowest_view_mode) 262 | /// to limit a `CalendarView` to only allow selection of days, months or years. 263 | /// 264 | /// Chainable variant. 265 | pub fn highest_view_mode(self, mode: ViewMode) -> Self { 266 | self.with(|v| v.set_highest_view_mode(mode)) 267 | } 268 | 269 | /// Sets and limits the earliest date selectable by this view. 270 | pub fn set_earliest_date(&mut self, date: Option>) { 271 | self.earliest_date = date; 272 | 273 | if let Some(ref date) = self.earliest_date { 274 | if self.date < *date { 275 | self.date = date.clone(); 276 | } 277 | } 278 | } 279 | 280 | /// Sets and limits the earliest date selectable by this view. 281 | /// 282 | /// Chainable variant. 283 | pub fn earliest_date(self, date: Option>) -> Self { 284 | self.with(|v| v.set_earliest_date(date)) 285 | } 286 | 287 | /// Sets and limits the latest date selectable by this view. 288 | pub fn set_latest_date(&mut self, date: Option>) { 289 | self.latest_date = date; 290 | 291 | if let Some(ref date) = self.latest_date { 292 | if self.date > *date { 293 | self.date = date.clone(); 294 | } 295 | } 296 | } 297 | 298 | /// Sets and limits the latest date selectable by this view. 299 | /// 300 | /// Chainable variant. 301 | pub fn latest_date(self, date: Option>) -> Self { 302 | self.with(|v| v.set_latest_date(date)) 303 | } 304 | 305 | /// Allows to change the default week start day of `WeekDay::Monday` to any other 306 | /// [`WeekDay`](struct.WeekDay.html). 307 | pub fn set_week_start(&mut self, day: WeekDay) { 308 | self.week_start = day; 309 | } 310 | 311 | /// Allows to change the default week start day of `WeekDay::Monday` to any other 312 | /// [`WeekDay`](struct.WeekDay.html). 313 | /// 314 | /// Chainable variant. 315 | pub fn week_start(self, day: WeekDay) -> Self { 316 | self.with(|v| v.set_week_start(day)) 317 | } 318 | 319 | /// Show or hide ISO week numbers in the `ViewMode::Month` view mode. 320 | /// 321 | /// ISO week numbers only make sense with a week start day of `WeekDay::Monday`. 322 | pub fn set_show_iso_weeks(&mut self, show: bool) { 323 | self.show_iso_weeks = show; 324 | } 325 | 326 | /// Show or hide ISO week numbers in the `ViewMode::Month` view mode. 327 | /// 328 | /// ISO week numbers only make sense with a week start day of `WeekDay::Monday`. 329 | /// 330 | /// Chainable variant. 331 | pub fn show_iso_weeks(self, show: bool) -> Self { 332 | self.with(|v| v.set_show_iso_weeks(show)) 333 | } 334 | 335 | /// Sets a callback to be used when `` is pressed to select a date. 336 | pub fn set_on_submit(&mut self, cb: F) 337 | where 338 | F: Fn(&mut Cursive, &Date) + Send + Sync + 'static, 339 | { 340 | self.on_submit = Some(Arc::new(move |s, date| cb(s, date))); 341 | } 342 | 343 | /// Sets a callback to be used when `` is pressed to select a date. 344 | /// 345 | /// Chainable variant. 346 | pub fn on_submit(self, cb: F) -> Self 347 | where 348 | F: Fn(&mut Cursive, &Date) + Send + Sync + 'static, 349 | { 350 | self.with(|v| v.set_on_submit(cb)) 351 | } 352 | 353 | /// Sets a callback to be used when an a new date is visually selected. 354 | pub fn set_on_select(&mut self, cb: F) 355 | where 356 | F: Fn(&mut Cursive, &Date) + Send + Sync + 'static, 357 | { 358 | self.on_select = Some(Arc::new(move |s, date| cb(s, date))); 359 | } 360 | 361 | /// Sets a callback to be used when an a new date is visually selected. 362 | /// 363 | /// Chainable variant. 364 | pub fn on_select(self, cb: F) -> Self 365 | where 366 | F: Fn(&mut Cursive, &Date) + Send + Sync + 'static, 367 | { 368 | self.with(|v| v.set_on_select(cb)) 369 | } 370 | } 371 | 372 | impl CalendarView 373 | where 374 | T::Offset: Send + Sync, 375 | { 376 | fn draw_month(&self, printer: &Printer<'_, '_>) { 377 | let year = self.view_date.year(); 378 | let month: Month = self.view_date.month0().into(); 379 | let month_start = self.view_date.with_day0(0).unwrap(); 380 | 381 | let active_day = self.date.day0() as i32; 382 | let view_day = self.view_date.day0() as i32; 383 | 384 | let d_month = self.date.month0() as i32 - self.view_date.month0() as i32; 385 | let d_year = self.date.year() - year; 386 | 387 | let month_days = month.number_of_days(year); 388 | let prev_month_days = month.prev_number_of_days(year); 389 | let first_week_day: WeekDay = (month_start.weekday() as i32).into(); 390 | 391 | // Draw Month Name 392 | printer.print( 393 | (0, 0), 394 | &format!( 395 | "{:^width$}", 396 | format!("{} {}", L::month(month, true), year), 397 | width = self.size.x 398 | ), 399 | ); 400 | 401 | // Draw Weekdays 402 | let h_offset = if self.show_iso_weeks { 3 } else { 0 }; 403 | let w_offset: i32 = self.week_start.into(); 404 | for i in 0..7 { 405 | let week_day: WeekDay = (i + w_offset).into(); 406 | printer.print((h_offset + i * 3, 1), L::week_day(week_day, false)); 407 | } 408 | 409 | // Draw days 410 | let d_shift = ((WeekDay::Monday as i32 - w_offset) + 7) % 7; 411 | let d_offset = ((first_week_day as i32) + d_shift) % 7; 412 | 413 | for (index, i) in (-d_offset..-d_offset + 42).enumerate() { 414 | let (day_number, month_offset) = if i < 0 { 415 | (prev_month_days + i, -1) 416 | } else if i > month_days - 1 { 417 | (i - month_days, 1) 418 | } else { 419 | (i, 0) 420 | }; 421 | 422 | if let Some(exact_date) = 423 | date_from_day_and_offsets(&self.view_date, Some(day_number), 0, month_offset, 0) 424 | { 425 | let color = if !self.date_available(&exact_date) { 426 | ColorStyle::tertiary() 427 | } else if i < 0 { 428 | if active_day == prev_month_days + i && d_month == -1 && d_year == 0 { 429 | if self.enabled && printer.focused { 430 | ColorStyle::highlight_inactive() 431 | } else { 432 | ColorStyle::secondary() 433 | } 434 | } else { 435 | ColorStyle::secondary() 436 | } 437 | } else if i > month_days - 1 { 438 | if active_day == i - month_days && d_month == 1 && d_year == 0 { 439 | if self.enabled && printer.focused { 440 | ColorStyle::highlight_inactive() 441 | } else { 442 | ColorStyle::secondary() 443 | } 444 | } else { 445 | ColorStyle::secondary() 446 | } 447 | } else if view_day == i { 448 | if self.enabled && printer.focused { 449 | ColorStyle::highlight() 450 | } else { 451 | ColorStyle::highlight_inactive() 452 | } 453 | } else if active_day == i && d_month == 0 && d_year == 0 { 454 | if self.enabled { 455 | ColorStyle::highlight_inactive() 456 | } else { 457 | ColorStyle::primary() 458 | } 459 | } else { 460 | ColorStyle::primary() 461 | }; 462 | 463 | // Draw day number 464 | let (x, y) = (h_offset + (index as i32 % 7) * 3, 2 + (index as i32 / 7)); 465 | printer.with_color(color, |printer| { 466 | printer.print((x, y), &format!("{:>2}", day_number + 1)); 467 | }); 468 | 469 | // Draw ISO Weeks (Only makes sense when start_of_week is Monday) 470 | if self.show_iso_weeks && index as i32 % 7 == 0 { 471 | let iso_week = exact_date.iso_week().week(); 472 | printer.with_color(ColorStyle::title_secondary(), |printer| { 473 | printer.print((0, y), &format!("{:>2}", iso_week)); 474 | }); 475 | } 476 | } 477 | } 478 | } 479 | 480 | fn draw_year(&self, printer: &Printer<'_, '_>) { 481 | let active_month = self.date.month0(); 482 | let view_month = self.view_date.month0(); 483 | let year = self.view_date.year(); 484 | let d_year = self.date.year() - year; 485 | 486 | // Draw Year 487 | printer.print( 488 | (0, 0), 489 | &format!("{:^width$}", format!("{}", year), width = self.size.x), 490 | ); 491 | 492 | // Draw Month Names 493 | let h_offset = if self.show_iso_weeks { 2 } else { 0 }; 494 | for i in 0..12 { 495 | let color = if !self.month_available(i, year) { 496 | ColorStyle::tertiary() 497 | } else if view_month == i { 498 | if self.enabled && printer.focused { 499 | ColorStyle::highlight() 500 | } else { 501 | ColorStyle::highlight_inactive() 502 | } 503 | } else if active_month == i && d_year == 0 { 504 | if self.enabled && printer.focused { 505 | ColorStyle::highlight_inactive() 506 | } else { 507 | ColorStyle::primary() 508 | } 509 | } else { 510 | ColorStyle::primary() 511 | }; 512 | 513 | let (x, y) = (h_offset + (i as i32 % 4) * 5, 2 + (i as i32 / 4) * 2); 514 | printer.with_color(color, |printer| { 515 | printer.print((x, y), &format!("{:>4}", L::month(i.into(), false))); 516 | }); 517 | } 518 | } 519 | 520 | fn draw_decade(&self, printer: &Printer<'_, '_>) { 521 | let active_year = self.date.year(); 522 | let view_year = self.view_date.year(); 523 | let decade = view_year - (view_year % 10); 524 | 525 | // Draw Year Range 526 | printer.print( 527 | (0, 0), 528 | &format!( 529 | "{:^width$}", 530 | format!("{} - {}", decade, decade + 9), 531 | width = self.size.x 532 | ), 533 | ); 534 | 535 | // Draw Years 536 | let h_offset = if self.show_iso_weeks { 2 } else { 0 }; 537 | for (index, i) in (-1..12).enumerate() { 538 | let year = decade + i; 539 | let color = if !self.year_available(year) { 540 | ColorStyle::tertiary() 541 | } else if !(0..=9).contains(&i) { 542 | if active_year == year { 543 | if self.enabled && printer.focused { 544 | ColorStyle::highlight_inactive() 545 | } else { 546 | ColorStyle::secondary() 547 | } 548 | } else { 549 | ColorStyle::secondary() 550 | } 551 | } else if view_year == year { 552 | if self.enabled && printer.focused { 553 | ColorStyle::highlight() 554 | } else { 555 | ColorStyle::highlight_inactive() 556 | } 557 | } else if active_year == year { 558 | if self.enabled { 559 | ColorStyle::highlight_inactive() 560 | } else { 561 | ColorStyle::primary() 562 | } 563 | } else { 564 | ColorStyle::primary() 565 | }; 566 | 567 | let (x, y) = ( 568 | h_offset + (index as i32 % 4) * 5, 569 | 2 + (index as i32 / 4) * 2, 570 | ); 571 | 572 | printer.with_color(color, |printer| { 573 | printer.print((x, y), &format!("{:>4}", year)); 574 | }); 575 | } 576 | } 577 | 578 | fn date_available(&self, date: &Date) -> bool { 579 | if let Some(ref earliest) = self.earliest_date { 580 | if *date < *earliest { 581 | return false; 582 | } 583 | } 584 | 585 | if let Some(ref latest) = self.latest_date { 586 | if *date > *latest { 587 | return false; 588 | } 589 | } 590 | 591 | true 592 | } 593 | 594 | fn month_available(&self, month: u32, year: i32) -> bool { 595 | if !self.year_available(year) { 596 | return false; 597 | } 598 | 599 | if let Some(ref earliest) = self.earliest_date { 600 | if year == earliest.year() && month < earliest.month0() { 601 | return false; 602 | } 603 | } 604 | 605 | if let Some(ref latest) = self.latest_date { 606 | if year == latest.year() && month > latest.month0() { 607 | return false; 608 | } 609 | } 610 | 611 | true 612 | } 613 | 614 | fn year_available(&self, year: i32) -> bool { 615 | if let Some(ref earliest) = self.earliest_date { 616 | if year < earliest.year() { 617 | return false; 618 | } 619 | } 620 | 621 | if let Some(ref latest) = self.latest_date { 622 | if year > latest.year() { 623 | return false; 624 | } 625 | } 626 | 627 | true 628 | } 629 | 630 | fn submit(&mut self) -> EventResult 631 | where 632 | T: 'static, 633 | { 634 | if self.view_mode == self.lowest_view_mode { 635 | self.date = self.view_date.clone(); 636 | 637 | if self.on_submit.is_some() { 638 | let cb = self.on_submit.clone().unwrap(); 639 | let date = self.date.clone(); 640 | return EventResult::Consumed(Some(Callback::from_fn(move |s| cb(s, &date)))); 641 | } 642 | } else { 643 | self.view_mode = match self.view_mode { 644 | ViewMode::Month | ViewMode::Year => ViewMode::Month, 645 | ViewMode::Decade => ViewMode::Year, 646 | }; 647 | } 648 | EventResult::Consumed(None) 649 | } 650 | } 651 | 652 | impl View 653 | for CalendarView 654 | where 655 | T::Offset: Send + Sync, 656 | { 657 | fn draw(&self, printer: &Printer<'_, '_>) { 658 | match self.view_mode { 659 | ViewMode::Month => self.draw_month(printer), 660 | ViewMode::Year => self.draw_year(printer), 661 | ViewMode::Decade => self.draw_decade(printer), 662 | } 663 | } 664 | 665 | fn required_size(&mut self, _: Vec2) -> Vec2 { 666 | self.size = if self.show_iso_weeks { 667 | (23, 8).into() 668 | } else { 669 | (20, 8).into() 670 | }; 671 | self.size 672 | } 673 | 674 | fn take_focus(&mut self, _: Direction) -> Result { 675 | self.enabled.then(EventResult::consumed).ok_or(CannotFocus) 676 | } 677 | 678 | fn on_event(&mut self, event: Event) -> EventResult { 679 | if !self.enabled { 680 | return EventResult::Ignored; 681 | } 682 | 683 | let last_view_date = self.view_date.clone(); 684 | let offsets = match event { 685 | Event::Key(Key::Up) => Some(match self.view_mode { 686 | ViewMode::Month => (-7, 0, 0), 687 | ViewMode::Year => (0, -4, 0), 688 | ViewMode::Decade => (0, 0, -4), 689 | }), 690 | Event::Key(Key::Down) => Some(match self.view_mode { 691 | ViewMode::Month => (7, 0, 0), 692 | ViewMode::Year => (0, 4, 0), 693 | ViewMode::Decade => (0, 0, 4), 694 | }), 695 | Event::Key(Key::Right) => Some(match self.view_mode { 696 | ViewMode::Month => (1, 0, 0), 697 | ViewMode::Year => (0, 1, 0), 698 | ViewMode::Decade => (0, 0, 1), 699 | }), 700 | Event::Key(Key::Left) => Some(match self.view_mode { 701 | ViewMode::Month => (-1, 0, 0), 702 | ViewMode::Year => (0, -1, 0), 703 | ViewMode::Decade => (0, 0, -1), 704 | }), 705 | Event::Key(Key::PageUp) => Some(match self.view_mode { 706 | ViewMode::Month => (0, -1, 0), 707 | ViewMode::Year => (0, 0, -1), 708 | ViewMode::Decade => (0, 0, -10), 709 | }), 710 | Event::Key(Key::PageDown) => Some(match self.view_mode { 711 | ViewMode::Month => (0, 1, 0), 712 | ViewMode::Year => (0, 0, 1), 713 | ViewMode::Decade => (0, 0, 10), 714 | }), 715 | Event::Key(Key::Backspace) => { 716 | if self.view_mode < self.highest_view_mode { 717 | self.view_mode = match self.view_mode { 718 | ViewMode::Month => ViewMode::Year, 719 | ViewMode::Year | ViewMode::Decade => ViewMode::Decade, 720 | }; 721 | } 722 | None 723 | } 724 | Event::Key(Key::Enter) => { 725 | return self.submit(); 726 | } 727 | Event::Mouse { 728 | position, 729 | offset, 730 | event: MouseEvent::Press(btn), 731 | } => { 732 | let position = match position.checked_sub(offset) { 733 | Some(position) => position, 734 | None => return EventResult::Ignored, 735 | }; 736 | match self.view_mode { 737 | ViewMode::Decade => { 738 | let h_offset = if self.show_iso_weeks { 2 } else { 0 }; 739 | if position.y < 2 740 | || position.y % 2 != 0 741 | || position.x < h_offset 742 | || (position.x - h_offset) % 5 == 4 743 | { 744 | return EventResult::Ignored; 745 | } 746 | let cell_index = (position.x - h_offset) / 5 + (position.y - 2) * 2; 747 | let current_index = 1 + last_view_date.year() % 10; 748 | 749 | let offset = cell_index as i32 - current_index; 750 | if offset == 0 && btn == MouseButton::Left { 751 | return self.submit(); 752 | } 753 | Some((0, 0, offset)) 754 | } 755 | ViewMode::Year => { 756 | let h_offset = if self.show_iso_weeks { 2 } else { 0 }; 757 | if position.y < 2 758 | || position.y % 2 != 0 759 | || position.x < h_offset 760 | || (position.x - h_offset) % 5 == 4 761 | { 762 | return EventResult::Ignored; 763 | } 764 | let month = 765 | 4 * (position.y.saturating_sub(2) / 2) + ((position.x - h_offset) / 5); 766 | let offset = month as i32 - last_view_date.month0() as i32; 767 | if offset == 0 && btn == MouseButton::Left { 768 | return self.submit(); 769 | } 770 | Some((0, offset, 0)) 771 | } 772 | ViewMode::Month => { 773 | let h_offset = if self.show_iso_weeks { 3 } else { 0 }; 774 | 775 | if position.y < 2 776 | || position.x < h_offset 777 | || (position.x - h_offset) % 3 == 2 778 | { 779 | return EventResult::Ignored; 780 | } 781 | 782 | let cell_index = (position.x - h_offset) / 3 + 7 * (position.y - 2); 783 | 784 | let month_start = self.view_date.with_day0(0).unwrap(); 785 | let first_week_day: WeekDay = (month_start.weekday() as i32).into(); 786 | let w_offset: i32 = self.week_start.into(); 787 | let d_shift = ((WeekDay::Monday as i32 - w_offset) + 7) % 7; 788 | let d_offset = ((first_week_day as i32) + d_shift) % 7; 789 | let current_index = last_view_date.day0() as i32 + d_offset; 790 | 791 | let offset = cell_index as i32 - current_index; 792 | if offset == 0 && btn == MouseButton::Left { 793 | return self.submit(); 794 | } 795 | Some((offset, 0, 0)) 796 | } 797 | } 798 | } 799 | _ => return EventResult::Ignored, 800 | }; 801 | 802 | if let Some((day, month, year)) = offsets { 803 | if let Some(date) = date_from_day_and_offsets(&last_view_date, None, day, month, year) { 804 | self.set_view_date(date); 805 | } 806 | } 807 | 808 | if self.view_date != last_view_date { 809 | let date = self.view_date.clone(); 810 | EventResult::Consumed( 811 | self.on_select 812 | .clone() 813 | .map(|cb| Callback::from_fn(move |s| cb(s, &date))), 814 | ) 815 | } else { 816 | EventResult::Consumed(None) 817 | } 818 | } 819 | } 820 | 821 | // Helpers -------------------------------------------------------------------- 822 | fn date_from_day_and_offsets( 823 | date: &Date, 824 | set_day: Option, 825 | day_offset: i32, 826 | month_offset: i32, 827 | year_offset: i32, 828 | ) -> Option> { 829 | let mut year = date.year() + year_offset; 830 | let mut month = date.month0() as i32; 831 | 832 | month += month_offset; 833 | 834 | while month < 0 { 835 | year -= 1; 836 | month += 12; 837 | } 838 | 839 | while month >= 12 { 840 | month -= 12; 841 | year += 1; 842 | } 843 | 844 | let d = date 845 | .with_day0(0)? 846 | .with_year(year)? 847 | .with_month0(month as u32)?; 848 | 849 | let month: Month = d.month0().into(); 850 | let number_of_days = month.number_of_days(year); 851 | 852 | let mut day = set_day.unwrap_or_else(|| cmp::min(number_of_days - 1, date.day0() as i32)); 853 | 854 | day += day_offset; 855 | if day < 0 { 856 | day += month.prev_number_of_days(year); 857 | date_from_day_and_offsets(&d, Some(day), 0, -1, 0) 858 | } else if day >= number_of_days { 859 | day -= number_of_days; 860 | date_from_day_and_offsets(&d, Some(day), 0, 1, 0) 861 | } else { 862 | d.with_day0(day as u32) 863 | } 864 | } 865 | 866 | #[test] 867 | fn test_offsets() { 868 | let date = Utc.ymd(1969, 7, 20); 869 | 870 | // Moon landing 871 | assert_eq!( 872 | Some(Utc.ymd(1969, 7, 20)), 873 | date_from_day_and_offsets(&date, None, 0, 0, 0) 874 | ); 875 | 876 | // Mission start 877 | assert_eq!( 878 | Some(Utc.ymd(1969, 7, 16)), 879 | date_from_day_and_offsets(&date, None, -4, 0, 0) 880 | ); 881 | 882 | // Mission end 883 | assert_eq!( 884 | Some(Utc.ymd(1969, 7, 24)), 885 | date_from_day_and_offsets(&date, None, 4, 0, 0) 886 | ); 887 | 888 | // Quarantine lifted 889 | assert_eq!( 890 | Some(Utc.ymd(1969, 8, 10)), 891 | date_from_day_and_offsets(&date, None, 21, 0, 0) 892 | ); 893 | assert_eq!( 894 | Some(Utc.ymd(1969, 8, 10)), 895 | date_from_day_and_offsets(&date, None, -10, 1, 0) 896 | ); 897 | } 898 | --------------------------------------------------------------------------------