├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── docs ├── base.css ├── index.css ├── index.html └── todomvc.js ├── src ├── main.rs ├── template-page.html └── template-todo.html ├── static ├── base.css ├── index.css └── index.html └── tools └── publish-site.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .gitattributes 3 | Cargo.lock 4 | .cargo 5 | static/todomvc* 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "todomvc" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "mustache 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 7 | "webplatform 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 8 | ] 9 | 10 | [[package]] 11 | name = "libc" 12 | version = "0.2.16" 13 | source = "registry+https://github.com/rust-lang/crates.io-index" 14 | 15 | [[package]] 16 | name = "log" 17 | version = "0.3.6" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | 20 | [[package]] 21 | name = "mustache" 22 | version = "0.7.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | dependencies = [ 25 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 26 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 27 | ] 28 | 29 | [[package]] 30 | name = "rustc-serialize" 31 | version = "0.3.19" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | 34 | [[package]] 35 | name = "webplatform" 36 | version = "0.4.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | dependencies = [ 39 | "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", 40 | ] 41 | 42 | [metadata] 43 | "checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d" 44 | "checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054" 45 | "checksum mustache 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "69ffd183990b7b56e7fde1581f19aa081ec52be0e31a42f47a9eca8d9bf56bf5" 46 | "checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b" 47 | "checksum webplatform 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "acc83fc21a39dadacde78257d56a3dccd4a6f48cf2d6028c69de683c122cc228" 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "todomvc" 4 | version = "0.1.0" 5 | license = "MIT" 6 | description = "TodoMVC example ported to Rust." 7 | authors = ["Tim Cameron Ryan "] 8 | 9 | [[bin]] 10 | name = "todomvc" 11 | 12 | [dependencies] 13 | rustc-serialize = "0.3.1" 14 | mustache = "0.7.0" 15 | webplatform = "0.4.0" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-todomvc 2 | 3 | [The TodoMVC app](https://github.com/tastejs/todomvc/blob/master/app-spec.md) 4 | implemented entirely in Rust using emscripten. 5 | 6 | Built on top of the [rust-webplatform](http://github.com/tcr/rust-webplatform) library. 7 | 8 | ## Compilation 9 | 10 | Compiling rust-todomvc for the browser requires a nightly Rust. 11 | 12 | ``` 13 | rustup install nightly 14 | rustup override set nightly 15 | rustup target add asmjs-unknown-emscripten 16 | rustup target add wasm32-unknown-emscripten 17 | ``` 18 | 19 | You should also set up emscripten: 20 | 21 | ``` 22 | curl -O https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz 23 | tar -xzf emsdk-portable.tar.gz 24 | source emsdk_portable/emsdk_env.sh 25 | emsdk update 26 | emsdk install sdk-incoming-64bit 27 | emsdk activate sdk-incoming-64bit 28 | ``` 29 | 30 | Then you're ready to build: 31 | 32 | ``` 33 | cargo build --target=asmjs-unknown-emscripten 34 | cp target/asmjs-unknown-emscripten/debug/todomvc.js static 35 | cd static; python -m SimpleHTTPServer 36 | ``` 37 | 38 | Open `http://localhost:8000/`. There you go! 39 | 40 | See [brson's post on Rust and emscripten](https://users.rust-lang.org/t/compiling-to-the-web-with-rust-and-emscripten/7627) for more installation details. 41 | 42 | ## License 43 | 44 | MIT or Apache-2.0, at your option. 45 | -------------------------------------------------------------------------------- /docs/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /docs/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | .todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | .todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | .new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | .new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | .main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | .toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | .toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | .todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | .todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | .todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | .todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | .todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | .todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | .todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | .todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Emscripten-Generated Code 7 | 14 | 15 | 16 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] extern crate webplatform; 2 | extern crate mustache; 3 | extern crate rustc_serialize; 4 | 5 | use mustache::{MapBuilder}; 6 | use std::rc::Rc; 7 | use std::cell::{RefCell}; 8 | use webplatform::{Event, LocalStorage}; 9 | use rustc_serialize::json; 10 | use std::clone::Clone; 11 | 12 | const TEMPLATE_PAGE: &'static str = include_str!("template-page.html"); 13 | const TEMPLATE_TODO: &'static str = include_str!("template-todo.html"); 14 | 15 | #[derive(RustcEncodable, RustcDecodable, Clone)] 16 | struct TodoItem { 17 | title: String, 18 | completed: bool, 19 | } 20 | 21 | impl TodoItem { 22 | fn toggle(&mut self) { 23 | self.completed = !self.completed; 24 | } 25 | } 26 | 27 | #[derive(Copy, Clone)] 28 | enum TodoState { 29 | Active, 30 | Completed, 31 | All 32 | } 33 | 34 | macro_rules! enclose { 35 | ( ($( $x:ident ),*) $y:expr ) => { 36 | { 37 | $(let $x = $x.clone();)* 38 | $y 39 | } 40 | }; 41 | } 42 | 43 | struct Todo { 44 | state: TodoState, 45 | items: Vec, 46 | } 47 | 48 | impl Todo { 49 | fn new() -> Todo { 50 | Todo { 51 | state: TodoState::All, 52 | items: vec![] 53 | } 54 | } 55 | } 56 | 57 | fn main() { 58 | let document = Rc::new(webplatform::init()); 59 | 60 | let body = document.element_query("body").unwrap(); 61 | body.class_add("learn-bar"); 62 | body.html_set(TEMPLATE_PAGE); 63 | 64 | let todo_new = document.element_query(".new-todo").unwrap(); 65 | let todo_count = document.element_query(".todo-count").unwrap(); 66 | let list = document.element_query(".todo-list").unwrap(); 67 | let clear = document.element_query(".clear-completed").unwrap(); 68 | let main = document.element_query(".main").unwrap(); 69 | let footer = document.element_query(".footer").unwrap(); 70 | let filter_all = document.element_query(".filters li:nth-child(1) a").unwrap(); 71 | let filter_active = document.element_query(".filters li:nth-child(2) a").unwrap(); 72 | let filter_completed = document.element_query(".filters li:nth-child(3) a").unwrap(); 73 | let toggle_all = document.element_query(".toggle-all").unwrap(); 74 | 75 | // Our TODO list. 76 | let todo = Rc::new(RefCell::new(Todo::new())); 77 | 78 | // Decode localStorage list of todos. 79 | if let Some(data) = LocalStorage.get("todos-rust") { 80 | if let Ok(vec) = json::decode::>(&data) { 81 | todo.borrow_mut().items.extend(vec.iter().cloned()); 82 | } 83 | } 84 | 85 | // Precompile mustache template for string. 86 | let template = mustache::compile_str(TEMPLATE_TODO); 87 | 88 | let llist = list.root_ref(); 89 | let render = Rc::new(enclose! { (todo) move || { 90 | LocalStorage.set("todos-rust", &json::encode(&todo.borrow().items).unwrap()); 91 | 92 | llist.html_set(""); 93 | 94 | for (i, item) in todo.borrow().items.iter().filter(|&x| { 95 | match todo.borrow().state { 96 | TodoState::All => true, 97 | TodoState::Active => !x.completed, 98 | TodoState::Completed => x.completed, 99 | } 100 | }).enumerate() { 101 | let data = MapBuilder::new() 102 | .insert_str("id", format!("{}", i)) 103 | .insert_str("checked", if item.completed { "checked" } else { "" }) 104 | .insert_str("value", item.title.clone()) 105 | .build(); 106 | 107 | let mut vec = Vec::new(); 108 | template.render_data(&mut vec, &data); 109 | llist.html_append(&String::from_utf8(vec).unwrap()); 110 | } 111 | 112 | let len = todo.borrow().items.iter().filter(|&x| !x.completed).count(); 113 | let leftstr = if len == 1 { 114 | "1 item left.".to_string() 115 | } else { 116 | format!("{} items left.", len) 117 | }; 118 | todo_count.html_set(&leftstr); 119 | 120 | main.style_set_str("display", if todo.borrow().items.len() == 0 { "none" } else { "block" }); 121 | footer.style_set_str("display", if todo.borrow().items.len() == 0 { "none" } else { "block" }); 122 | 123 | match todo.borrow().state { 124 | TodoState::All => { 125 | filter_all.class_add("selected"); 126 | filter_active.class_remove("selected"); 127 | filter_completed.class_remove("selected"); 128 | }, 129 | TodoState::Active => { 130 | filter_all.class_remove("selected"); 131 | filter_active.class_add("selected"); 132 | filter_completed.class_remove("selected"); 133 | }, 134 | TodoState::Completed => { 135 | filter_all.class_remove("selected"); 136 | filter_active.class_remove("selected"); 137 | filter_completed.class_add("selected"); 138 | }, 139 | } 140 | } }); 141 | 142 | list.on("click", enclose! { (todo, render) move |e:Event| { 143 | let node = e.target.unwrap(); 144 | if node.class_get().contains("destroy") { 145 | let id = node.parent().unwrap().parent().unwrap().data_get("id").unwrap().parse::().unwrap(); 146 | todo.borrow_mut().items.remove(id); 147 | render(); 148 | } else if node.class_get().contains("toggle") { 149 | let id = node.parent().unwrap().parent().unwrap().data_get("id").unwrap().parse::().unwrap(); 150 | todo.borrow_mut().items[id].toggle(); 151 | render(); 152 | } 153 | } }); 154 | 155 | list.on("dblclick", enclose! { (document) move |e:Event| { 156 | let node = e.target.unwrap(); 157 | if node.tagname() == "label" { 158 | node.parent().unwrap().parent().unwrap().class_add("editing"); 159 | document.element_query("li.editing .edit").unwrap().focus(); 160 | } 161 | } }); 162 | 163 | list.captured_on("blur", enclose! { (todo, render) move |e:Event| { 164 | let node = e.target.unwrap(); 165 | if node.class_get().contains("edit") { 166 | let id = node.parent().unwrap().data_get("id").unwrap().parse::().unwrap(); 167 | todo.borrow_mut().items[id].title = node.prop_get_str("value"); 168 | render(); 169 | } 170 | } }); 171 | 172 | clear.on("click", enclose! { (todo, render) move |_:Event| { 173 | todo.borrow_mut().items.retain(|ref x| !x.completed); 174 | render(); 175 | } }); 176 | 177 | let t1 = todo_new.root_ref(); 178 | todo_new.on("change", enclose! { (todo, render) move |_:Event| { 179 | let value = t1.prop_get_str("value"); 180 | t1.prop_set_str("value", ""); 181 | 182 | todo.borrow_mut().items.push(TodoItem { 183 | title: value, 184 | completed: false, 185 | }); 186 | render(); 187 | } }); 188 | 189 | let update_path = Rc::new(enclose! { (render, todo, document) move || { 190 | let hash = document.location_hash_get(); 191 | let path = if hash.len() < 1 { 192 | vec!["".to_string()] 193 | } else { 194 | hash[1..].split("/").filter(|x| x.len() > 0).map(|x| x.to_string()).collect::>() 195 | }; 196 | 197 | match &*path[0] { 198 | "active" => todo.borrow_mut().state = TodoState::Active, 199 | "completed" => todo.borrow_mut().state = TodoState::Completed, 200 | _ => todo.borrow_mut().state = TodoState::All, 201 | } 202 | 203 | render(); 204 | } }); 205 | 206 | document.on("hashchange", enclose! { (update_path) move |_:Event| { 207 | update_path(); 208 | } }); 209 | update_path(); 210 | 211 | let tgl = toggle_all.root_ref(); 212 | toggle_all.on("change", enclose! { (todo, render) move |_:Event| { 213 | let val = if tgl.prop_get_i32("checked") == 1 { true } else { false }; 214 | for item in todo.borrow_mut().items.iter_mut() { 215 | item.completed = val; 216 | } 217 | render(); 218 | } }); 219 | 220 | render(); 221 | webplatform::spin(); 222 | } 223 | -------------------------------------------------------------------------------- /src/template-page.html: -------------------------------------------------------------------------------- 1 | Rust · TodoMVC 2 | 3 | 4 | 17 |
18 |
19 |

todos

20 | 21 |
22 |
23 | 24 | 25 |
    26 |
    27 |
    28 | 29 | 40 | 41 |
    42 |
    43 | 49 | -------------------------------------------------------------------------------- /src/template-todo.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 |
    7 | 8 |
  • 9 | -------------------------------------------------------------------------------- /static/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /static/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | .todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | .todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | .new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | .new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | .main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | .toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | .toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | .todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | .todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | .todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | .todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | .todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | .todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | .todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | .todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Emscripten-Generated Code 7 | 14 | 15 | 16 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /tools/publish-site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd $(dirname $0)/.. 6 | make 7 | git add -f static/* 8 | git commit -am "Publishes site update." 9 | git push origin `git subtree split --prefix static master`:refs/heads/gh-pages --force 10 | git reset HEAD~1 11 | --------------------------------------------------------------------------------