├── .gitignore ├── assets ├── demo.gif ├── cursive-multiplex.png ├── README.md ├── config.yml └── cursive-multiplex.svg ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── test_and_push.yml ├── Cargo.toml ├── scripts ├── shields-from-tests.jq ├── ci-build.sh └── deploy-badges.sh ├── src ├── error.rs ├── path.rs ├── node.rs ├── id.rs ├── actions.rs └── lib.rs ├── LICENSE ├── examples ├── skewed.rs ├── tily.rs ├── history.rs └── basic.rs ├── tests ├── snapshots │ ├── end2end__up once.snap │ ├── end2end__up twice.snap │ ├── end2end__down once.snap │ ├── end2end__down thrice.snap │ ├── end2end__down twice.snap │ ├── end2end__end2end_zoom.snap │ ├── end2end__left once.snap │ ├── end2end__left thrice.snap │ ├── end2end__left twice.snap │ ├── end2end__right once.snap │ ├── end2end__right thrice.snap │ ├── end2end__right twice.snap │ ├── end2end__up thrice.snap │ ├── end2end__end2end_complex_resize.snap │ ├── end2end__end2end_down_resize.snap │ ├── end2end__end2end_vertical_resize.snap │ ├── end2end__end2end_smoke.snap │ ├── end2end__end2end_complex.snap │ ├── end2end__end2end_horizontal.snap │ ├── end2end__end2end_left_right_focus_history.snap │ ├── end2end__end2end_up_down_focus_history.snap │ ├── end2end__end2end_vertical.snap │ ├── end2end__end2end_complex_focus.snap │ ├── end2end__end2end_complex_remove.snap │ ├── end2end__end2end_complex_switch.snap │ ├── end2end__end2end_horizontal_fixed_size.snap │ ├── end2end__end2end_switch_views.snap │ ├── end2end__end2end_vertical_remove.snap │ ├── end2end__end2end_custom_split_ratio_horizontal.snap │ ├── end2end__end2end_custom_split_ratio_overshoot.snap │ ├── end2end__end2end_custom_split_ratio_undershoot.snap │ ├── end2end__end2end_custom_split_ratio_vertical.snap │ ├── end2end__end2end_horizontal_remove.snap │ ├── end2end__end2end_horizontal_switch.snap │ ├── end2end__end2end_vertical_fixed_size.snap │ └── end2end__end2end_custom_split_ratio_overshoot_correction.snap ├── geometric.rs └── end2end.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | node_modules 5 | *.temp 6 | .org 7 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deinstapel/cursive-multiplex/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /assets/cursive-multiplex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deinstapel/cursive-multiplex/HEAD/assets/cursive-multiplex.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [{*.json,*.yml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # Creating a new GIF 2 | 3 | ## Recording 4 | 5 | Inside a `80x24` terminal record it using 6 | 7 | ``` 8 | $ cargo build --example basic 9 | $ cd assets 10 | $ terminalizer record --config ./config.yml demo 11 | ``` 12 | 13 | ## Rendering 14 | 15 | ``` 16 | $ terminalizer render demo.yml -o demo.gif 17 | ``` 18 | 19 | ## Optimizing 20 | 21 | ``` 22 | $ gifsicle --colors 24 -O3 demo.gif -o demo.gif 23 | ``` 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [fin-ger, jwuensche] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Fin Christensen ", "Johannes Wünsche "] 3 | description = "A tmux like multiplexer for gyscos/cursive views" 4 | documentation = "https://docs.rs/cursive-multiplex" 5 | edition = "2018" 6 | keywords = ["cursive", "tui", "terminal", "tiling", "view"] 7 | license = "BSD-3-Clause" 8 | name = "cursive-multiplex" 9 | readme = "README.md" 10 | repository = "https://github.com/deinstapel/cursive-multiplex" 11 | version = "0.7.0" 12 | 13 | [dependencies] 14 | cursive_core = "0.4" 15 | thiserror = "1" 16 | indextree = "4.3" 17 | log = "0.4" 18 | 19 | [dev-dependencies] 20 | crossbeam = "0.8" 21 | cursive = "0.21" 22 | serde_json = "1" 23 | insta = "1.10.0" 24 | -------------------------------------------------------------------------------- /scripts/shields-from-tests.jq: -------------------------------------------------------------------------------- 1 | { 2 | # filter for "passed" and "test_count" in input objects with `"type": "suite"` 3 | # and accumulate stats from all tests 4 | "passed": map(select(.type == "suite" and has("passed")) | .passed) | add, 5 | "total": map(select(.type == "suite" and has("test_count")) | .test_count) | add 6 | } | . + { 7 | # calculate ratio of passed tests 8 | "factor": (.passed / .total) 9 | } | { 10 | # calculate color from test factor 11 | "color": ( 12 | if .factor < 0.33 then 13 | "red" 14 | elif .factor < 0.66 then 15 | "orange" 16 | elif .factor < 1.0 then 17 | "yellow" 18 | else 19 | "brightgreen" 20 | end 21 | ), 22 | "isError": true, 23 | "label": "cargo test", 24 | # interpolate the shield label 25 | "message": "\(.passed) / \(.total) tests", 26 | "schemaVersion": 1 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/test_and_push.yml: -------------------------------------------------------------------------------- 1 | name: "test_and_badge" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | jobs: 10 | test_and_badge: 11 | name: "Test and Badge" 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - name: "Prepare" 15 | run: | 16 | sudo apt-get update 17 | sudo apt-get install -y curl git 18 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 19 | source $HOME/.cargo/env 20 | rustup toolchain install nightly 21 | git clone https://github.com/deinstapel/cursive-multiplex.git 22 | 23 | - name: "Build and Test" 24 | run: | 25 | cd cursive-multiplex 26 | RUST_CHAIN=stable ./scripts/ci-build.sh 27 | RUST_CHAIN=nightly ./scripts/ci-build.sh 28 | 29 | - name: "Deploy Badge" 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: | 33 | cd cursive-multiplex 34 | ./scripts/deploy-badges.sh 35 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::Id; 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Error)] 5 | pub enum AddViewError { 6 | #[error("some error occured")] 7 | GenericError {}, 8 | } 9 | 10 | #[derive(Debug, Error)] 11 | pub enum RemoveViewError { 12 | #[error("invalid id given, cannot be removed: {}", id)] 13 | InvalidId { id: Id }, 14 | 15 | #[error("id has no parent, cannot be removed: {}", id)] 16 | NoParent { id: Id }, 17 | 18 | #[error("something broke, oh no ")] 19 | Generic {}, 20 | } 21 | 22 | #[derive(Debug, Error)] 23 | pub enum SwitchError { 24 | #[error("node {} has no parent to be switched to from {}", from, to)] 25 | NoParent { from: Id, to: Id }, 26 | 27 | #[error("error while switching, figuring out...")] 28 | Failed {}, 29 | } 30 | 31 | #[derive(Debug, Error)] 32 | pub enum RenderError { 33 | #[error("encountered arithmetic error")] 34 | Arithmetic {}, 35 | } 36 | 37 | impl std::convert::From for SwitchError { 38 | fn from(_error: indextree::NodeError) -> Self { 39 | SwitchError::Failed {} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scripts/ci-build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | die() { 4 | printf "\e[31:1mError: %s\e[0m\n" "$1" >&2 5 | exit 1 6 | } 7 | 8 | if [ -z "$RUST_CHAIN" ] 9 | then 10 | die "RUST_CHAIN environment variable is not set! RUST_CHAIN={stable,nightly}" 11 | fi 12 | 13 | ( 14 | cd "$(git rev-parse --show-toplevel)" || die "cannot find project root" 15 | 16 | # Badges! 17 | mkdir -p ./target/shields 18 | if cargo "+${RUST_CHAIN}" --color=always build --all-targets; then 19 | cat < "./target/shields/$RUST_CHAIN-build.json" 20 | { 21 | "color": "brightgreen", 22 | "isError": true, 23 | "label": "$RUST_CHAIN build", 24 | "message": "passing", 25 | "schemaVersion": 1 26 | } 27 | EOF 28 | else 29 | PRV_EXIT=$? 30 | cat < "./target/shields/$RUST_CHAIN-build.json" 31 | { 32 | "color": "red", 33 | "isError": true, 34 | "label": "$RUST_CHAIN build", 35 | "message": "failed", 36 | "schemaVersion": 1 37 | } 38 | EOF 39 | exit $PRV_EXIT 40 | fi 41 | 42 | cargo "+${RUST_CHAIN}" --color=always test --no-fail-fast 43 | exitcode=$? 44 | 45 | # create badge for `cargo test` 46 | cargo "+${RUST_CHAIN}" test --no-fail-fast -- -Z unstable-options --format json | \ 47 | jq -s -f ./scripts/shields-from-tests.jq > ./target/shields/cargo-test.json 48 | 49 | exit $exitcode 50 | ) 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, deinstapel 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /examples/skewed.rs: -------------------------------------------------------------------------------- 1 | use cursive::Cursive; 2 | use cursive_multiplex::Mux; 3 | 4 | fn main() { 5 | let mut siv = cursive::default(); 6 | // siv.show_debug_console(); 7 | let mut mux = Mux::new().with_default_split_ratio(0.7); 8 | let node1 = mux 9 | .add_right_of( 10 | cursive_core::views::ResizedView::with_full_screen(cursive_core::views::TextArea::new()), 11 | mux.root().build().unwrap(), 12 | ) 13 | .expect("text view failed"); 14 | 15 | let mut menubar = cursive_core::views::Menubar::new(); 16 | menubar.add_leaf("Hello from cursive_multiplex", |_| {}); 17 | menubar.add_leaf("Feel free to try out the examples simply with `cargo run --example=basic` or `cargo run --example=tily`", |_|{}); 18 | 19 | let node2 = mux 20 | .add_right_of( 21 | cursive_core::views::ResizedView::with_full_screen(cursive_core::views::TextArea::new()), 22 | node1, 23 | ) 24 | .unwrap(); 25 | let _ = mux 26 | .add_below( 27 | cursive_core::views::ResizedView::with_full_screen(cursive_core::views::TextArea::new()), 28 | node2, 29 | ) 30 | .unwrap(); 31 | 32 | let idlayer = cursive_core::views::NamedView::new("Mux", mux); 33 | let mut linear = 34 | cursive_core::views::LinearLayout::new(cursive_core::direction::Orientation::Vertical); 35 | 36 | linear.add_child(idlayer); 37 | linear.add_child(menubar); 38 | siv.add_fullscreen_layer(linear); 39 | siv.add_global_callback('q', Cursive::quit); 40 | cursive_core::logger::init(); 41 | siv.run(); 42 | } 43 | -------------------------------------------------------------------------------- /scripts/deploy-badges.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | die() { 4 | printf "\e[31:1mError: %s\e[0m\n" "$1" >&2 5 | exit 1 6 | } 7 | 8 | if [ -z "$GITHUB_ACTOR" ] 9 | then 10 | die "the GITHUB_ACTOR environment variable is not set" 11 | fi 12 | 13 | if [ -z "$GITHUB_TOKEN" ] 14 | then 15 | die "the GITHUB_TOKEN environment variable is not set" 16 | fi 17 | 18 | if [ -z "$GITHUB_REPOSITORY" ] 19 | then 20 | die "the GITHUB_REPOSITORY environment variable is not set" 21 | fi 22 | 23 | ( 24 | cd "$(git rev-parse --show-toplevel)/target/shields" || die "cannot find project root!" 25 | repo="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 26 | tmp_dir=$(mktemp -d -t cursive-multiplex-deploy-XXXXXXXX) 27 | 28 | git config --global user.email "runner@ci" 29 | git config --global user.name "Github CI Runner" 30 | try=0 31 | while :; do 32 | if ! git clone --branch gh-pages "$repo" "$tmp_dir" 33 | then 34 | ( 35 | cd "$tmp_dir" || die "failed to enter temporary directory" 36 | git init 37 | git remote add origin "$repo" 38 | git checkout -b gh-pages 39 | ) 40 | fi 41 | 42 | cp -ar ./* "$tmp_dir" 43 | 44 | ( 45 | cd "$tmp_dir" || die "failed to enter temporary directory" 46 | git add -A 47 | git commit -m "Github CI badge deployment" 48 | git push origin gh-pages:gh-pages 49 | ) 50 | 51 | result=$? 52 | if [ "$result" -eq 0 ] || [ "$try" -ge 5 ] 53 | then 54 | break 55 | fi 56 | 57 | try=$((try + 1)) 58 | done 59 | 60 | rm -rf "$tmp_dir" 61 | 62 | ) 63 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__up once.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6────────────────────────────────────────────────────────────────────────────────| 14 | 7_ | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__up twice.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0_ | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6────────────────────────────────────────────────────────────────────────────────| 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__down once.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6────────────────────────────────────────────────────────────────────────────────| 14 | 7_ | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__down thrice.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6────────────────────────────────────────────────────────────────────────────────| 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3_ | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__down twice.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6────────────────────────────────────────────────────────────────────────────────| 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3_ | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_zoom.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Ups asd | 8 | 1 asd | 9 | 2 asd asd | 10 | 3 as a | 11 | 4 s da | 12 | 5 s a | 13 | 6 sdasdasdasdasdfasfgarhbah | 14 | 7 ga | 15 | 8 fa | 16 | 9 sdf | 17 | 0 asf | 18 | 1 | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 a | 23 | 6 sdfa | 24 | 7 sdf | 25 | 8 ad | 26 | 9 fas | 27 | 0 f | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__left once.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 │_ │ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6 │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2 │ │ | 20 | 3 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__left thrice.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0_ │ │ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6 │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2 │ │ | 20 | 3 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__left twice.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0_ │ │ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6 │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2 │ │ | 20 | 3 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__right once.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 │_ │ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6 │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2 │ │ | 20 | 3 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__right thrice.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 │ │_ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6 │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2 │ │ | 20 | 3 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__right twice.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 │ │_ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6 │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2 │ │ | 20 | 3 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__up thrice.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0_ | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6────────────────────────────────────────────────────────────────────────────────| 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_complex_resize.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 │ │ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5───────────────────────────────────────│ │ | 13 | 6_ │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2───────────────────────────────────────│ │ | 20 | 3 │────────────────────────────────────────| 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_down_resize.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0_ │ │ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6 │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2 │ │ | 20 | 3 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_vertical_resize.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0_ | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1────────────────────────────────────────────────────────────────────────────────| 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8────────────────────────────────────────────────────────────────────────────────| 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_smoke.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 Hello World | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_complex.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0left 1 │right 1 │right 2 | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6────────────────────────────────────────│ │ | 14 | 7left 3 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2────────────────────────────────────────│───────────────────────────────────────| 20 | 3left 2 │right 3 | 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_horizontal.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Left │Center │Right | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6 │ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2 │ │ | 20 | 3 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_left_right_focus_history.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 │ | 8 | 1 │ | 9 | 2 │ | 10 | 3 │ | 11 | 4 │ | 12 | 5 │ | 13 | 6 │ | 14 | 7 │ | 15 | 8 │ | 16 | 9 │ | 17 | 0 │ | 18 | 1 │ | 19 | 2 │───────────────────────────────────────| 20 | 3 │_ | 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_up_down_focus_history.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3 │_ | 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_vertical.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Up | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6────────────────────────────────────────────────────────────────────────────────| 14 | 7Center | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3Down | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_complex_focus.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 │ │_ | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6────────────────────────────────────────│ │ | 14 | 7 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2────────────────────────────────────────│───────────────────────────────────────| 20 | 3 │ | 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_complex_remove.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0left 1 │right 1 │right 2 | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6────────────────────────────────────────│ │ | 14 | 7left 3 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2────────────────────────────────────────│ │ | 20 | 3left 2 │ │ | 21 | 4 │ │ | 22 | 5 │ │ | 23 | 6 │ │ | 24 | 7 │ │ | 25 | 8 │ │ | 26 | 9 │ │ | 27 | 0 │ │ | 28 | 1 │ │ | 29 | 2 │ │ | 30 | 3 │ │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_complex_switch.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0left 1 │left 3 │right 2 | 8 | 1 │ │ | 9 | 2 │ │ | 10 | 3 │ │ | 11 | 4 │ │ | 12 | 5 │ │ | 13 | 6────────────────────────────────────────│ │ | 14 | 7right 1 │ │ | 15 | 8 │ │ | 16 | 9 │ │ | 17 | 0 │ │ | 18 | 1 │ │ | 19 | 2────────────────────────────────────────│───────────────────────────────────────| 20 | 3left 2 │right 3 | 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_horizontal_fixed_size.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Root │┌─────────────────────────────────────┐| 8 | 1 ││Fixed │| 9 | 2 ││ │| 10 | 3 ││ │| 11 | 4 ││ │| 12 | 5 ││ │| 13 | 6 ││ │| 14 | 7 ││ │| 15 | 8 ││ │| 16 | 9 ││ │| 17 | 0 │└─────────────────────────────────────┘| 18 | 1 │ | 19 | 2 │ | 20 | 3 │ | 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_switch_views.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Up | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3Down | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_vertical_remove.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Up | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3Down | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_custom_split_ratio_horizontal.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0A very very long text to demonstrate the split that happens │_ | 9 | 1later on in this example. │ | 10 | 2 │ | 11 | 3 │ | 12 | 4 │ | 13 | 5 │ | 14 | 6 │ | 15 | 7 │ | 16 | 8 │ | 17 | 9 │ | 18 | 0 │ | 19 | 1 │ | 20 | 2 │ | 21 | 3 │ | 22 | 4 │ | 23 | 5 │ | 24 | 6 │ | 25 | 7 │ | 26 | 8 │ | 27 | 9 │ | 28 | 0 │ | 29 | 1 │ | 30 | 2 │ | 31 | 3 │ | 32 | x--------------------------------------------------------------------------------x 33 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_custom_split_ratio_overshoot.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0A very very long text to demonstrate the split that happens later on in this | 9 | 1example. | 10 | 2 | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 20 | 2 | 21 | 3 | 22 | 4 | 23 | 5 | 24 | 6 | 25 | 7 | 26 | 8 | 27 | 9 | 28 | 0 | 29 | 1 | 30 | 2 | 31 | 3 | 32 | x--------------------------------------------------------------------------------x 33 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_custom_split_ratio_undershoot.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0│ | 9 | 1│ | 10 | 2│ | 11 | 3│ | 12 | 4│ | 13 | 5│ | 14 | 6│ | 15 | 7│ | 16 | 8│ | 17 | 9│ | 18 | 0│ | 19 | 1│ | 20 | 2│ | 21 | 3│ | 22 | 4│ | 23 | 5│ | 24 | 6│ | 25 | 7│ | 26 | 8│ | 27 | 9│ | 28 | 0│ | 29 | 1│ | 30 | 2│ | 31 | 3│ | 32 | x--------------------------------------------------------------------------------x 33 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_custom_split_ratio_vertical.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0_ | 9 | 1 | 10 | 2 | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 20 | 2 | 21 | 3 | 22 | 4 | 23 | 5 | 24 | 6 | 25 | 7 | 26 | 8 | 27 | 9────────────────────────────────────────────────────────────────────────────────| 28 | 0A very very long text to demonstrate the split that happens later on in this | 29 | 1example. | 30 | 2 | 31 | 3 | 32 | x--------------------------------------------------------------------------------x 33 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_horizontal_remove.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Left │Right | 8 | 1 │ | 9 | 2 │ | 10 | 3 │ | 11 | 4 │ | 12 | 5 │ | 13 | 6 │ | 14 | 7 │ | 15 | 8 │ | 16 | 9 │ | 17 | 0 │ | 18 | 1 │ | 19 | 2 │ | 20 | 3 │ | 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_horizontal_switch.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Left │Right | 8 | 1 │ | 9 | 2 │ | 10 | 3 │ | 11 | 4 │ | 12 | 5 │ | 13 | 6 │ | 14 | 7 │ | 15 | 8 │ | 16 | 9 │ | 17 | 0 │ | 18 | 1 │ | 19 | 2 │ | 20 | 3 │ | 21 | 4 │ | 22 | 5 │ | 23 | 6 │ | 24 | 7 │ | 25 | 8 │ | 26 | 9 │ | 27 | 0 │ | 28 | 1 │ | 29 | 2 │ | 30 | 3 │ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_vertical_fixed_size.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0Root | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2────────────────────────────────────────────────────────────────────────────────| 20 | 3┌────────────────────────────────────────┐ | 21 | 4│Fixed │ | 22 | 5│ │ | 23 | 6│ │ | 24 | 7│ │ | 25 | 8│ │ | 26 | 9│ │ | 27 | 0│ │ | 28 | 1│ │ | 29 | 2│ │ | 30 | 3└────────────────────────────────────────┘ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_custom_split_ratio_overshoot_correction.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: tsiv.last_screen() 4 | 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0A very very long text to demonstrate the│_ | 9 | 1split that happens later on in this │ | 10 | 2example. │ | 11 | 3 │ | 12 | 4 │ | 13 | 5 │ | 14 | 6 │ | 15 | 7 │ | 16 | 8 │ | 17 | 9 │ | 18 | 0 │ | 19 | 1 │ | 20 | 2 │ | 21 | 3 │ | 22 | 4 │ | 23 | 5 │ | 24 | 6 │ | 25 | 7 │ | 26 | 8 │ | 27 | 9 │ | 28 | 0 │ | 29 | 1 │ | 30 | 2 │ | 31 | 3 │ | 32 | x--------------------------------------------------------------------------------x 33 | 34 | -------------------------------------------------------------------------------- /examples/tily.rs: -------------------------------------------------------------------------------- 1 | use cursive_core::views::{ResizedView, TextArea, TextView}; 2 | use cursive_core::Cursive; 3 | use cursive_multiplex::{Id, Mux}; 4 | 5 | fn main() { 6 | let mut siv = cursive::default(); 7 | siv.show_debug_console(); 8 | let mut mux = Mux::new(); 9 | let top_left_corner = mux 10 | .add_right_of( 11 | ResizedView::with_full_screen(TextArea::new()), 12 | mux.root().build().unwrap(), 13 | ) 14 | .expect("first failed"); 15 | 16 | let top_right_mid = mux 17 | .add_right_of( 18 | ResizedView::with_full_screen(TextArea::new()), 19 | top_left_corner, 20 | ) 21 | .unwrap(); 22 | let bottom_right_mid = mux 23 | .add_below( 24 | ResizedView::with_full_screen(TextView::new("I will not be focused!")), 25 | top_right_mid, 26 | ) 27 | .unwrap(); 28 | let _ = mux 29 | .add_right_of( 30 | cursive_core::views::Panel::new(ResizedView::with_full_screen(TextArea::new())), 31 | top_right_mid, 32 | ) 33 | .unwrap(); 34 | let _ = mux 35 | .add_right_of( 36 | ResizedView::with_full_screen(TextArea::new()), 37 | bottom_right_mid, 38 | ) 39 | .unwrap(); 40 | let bottom_left_corner = mux 41 | .add_below( 42 | ResizedView::with_full_screen(TextArea::new()), 43 | top_left_corner, 44 | ) 45 | .unwrap(); 46 | let top_left_mid = mux 47 | .add_right_of( 48 | ResizedView::with_full_screen(TextArea::new()), 49 | top_left_corner, 50 | ) 51 | .unwrap(); 52 | let _ = mux 53 | .add_right_of( 54 | cursive_core::views::Panel::new(ResizedView::with_full_screen(TextArea::new())), 55 | bottom_left_corner, 56 | ) 57 | .unwrap(); 58 | 59 | let idlayer = cursive_core::views::NamedView::new("Steven", mux); 60 | 61 | let boxes = cursive_core::views::ResizedView::new( 62 | cursive_core::view::SizeConstraint::Full, 63 | cursive_core::view::SizeConstraint::Full, 64 | idlayer, 65 | ); 66 | 67 | siv.add_fullscreen_layer(boxes); 68 | siv.add_global_callback('q', Cursive::quit); 69 | siv.add_global_callback('e', move |ref mut siv| { 70 | add_plane(siv, top_left_mid); 71 | }); 72 | cursive_core::logger::init(); 73 | siv.run(); 74 | } 75 | 76 | fn add_plane(siv: &mut Cursive, node: Id) { 77 | let mut foo: cursive_core::views::ViewRef = siv.find_name("Steven").unwrap(); 78 | foo.add_below( 79 | cursive_core::views::TextView::new("Dynamic!".to_string()), 80 | node, 81 | ) 82 | .unwrap(); 83 | } 84 | -------------------------------------------------------------------------------- /examples/history.rs: -------------------------------------------------------------------------------- 1 | use cursive::Cursive; 2 | use cursive_multiplex::Mux; 3 | 4 | fn main() { 5 | let mut siv = cursive::default(); 6 | // siv.show_debug_console(); 7 | let mut mux = Mux::new(); 8 | let node1 = mux 9 | .add_right_of( 10 | cursive_core::views::ResizedView::with_full_screen(cursive_core::views::TextArea::new()), 11 | mux.root().build().unwrap(), 12 | ) 13 | .expect("text view failed"); 14 | 15 | let mut menubar = cursive_core::views::Menubar::new(); 16 | menubar.add_leaf("Hello from cursive_multiplex", |_| {}); 17 | menubar.add_leaf("Feel free to try out the examples simply with `cargo run --example=basic` or `cargo run --example=tily`", |_|{}); 18 | 19 | let node2 = mux 20 | .add_right_of( 21 | cursive_core::views::ResizedView::with_full_screen(cursive_core::views::TextArea::new()), 22 | node1, 23 | ) 24 | .unwrap(); 25 | let _ = mux 26 | .add_below( 27 | cursive_core::views::ResizedView::with_full_screen(cursive_core::views::TextArea::new()), 28 | node2, 29 | ) 30 | .unwrap(); 31 | 32 | let idlayer = cursive_core::views::NamedView::new("Mux", mux); 33 | let mut linear = 34 | cursive_core::views::LinearLayout::new(cursive_core::direction::Orientation::Vertical); 35 | 36 | linear.add_child(idlayer); 37 | linear.add_child(menubar); 38 | siv.add_fullscreen_layer(linear); 39 | siv.add_global_callback('q', Cursive::quit); 40 | siv.add_global_callback( 41 | cursive_core::event::Event::Alt(cursive_core::event::Key::Ins), 42 | move |ref mut siv| { 43 | add_pane(siv); 44 | }, 45 | ); 46 | siv.add_global_callback( 47 | cursive_core::event::Event::Alt(cursive_core::event::Key::Del), 48 | move |ref mut siv| { 49 | remove_pane(siv); 50 | }, 51 | ); 52 | cursive_core::logger::init(); 53 | siv.run(); 54 | } 55 | 56 | fn add_pane(siv: &mut Cursive) { 57 | let mut mux: cursive_core::views::ViewRef = siv.find_name("Mux").unwrap(); 58 | let surprise = "⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⣠⣤⣶⣶ 59 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⢰⣿⣿⣿⣿ 60 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣀⣀⣾⣿⣿⣿⣿ 61 | ⣿⣿⣿⣿⣿⡏⠉⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿ 62 | ⣿⣿⣿⣿⣿⣿⠀⠀⠀⠈⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠉⠁⠀⣿ 63 | ⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠙⠿⠿⠿⠻⠿⠿⠟⠿⠛⠉⠀⠀⠀⠀⠀⣸⣿ 64 | ⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿ 65 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣴⣿⣿⣿⣿ 66 | ⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⢰⣹⡆⠀⠀⠀⠀⠀⠀⣭⣷⠀⠀⠀⠸⣿⣿⣿⣿ 67 | ⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠈⠉⠀⠀⠤⠄⠀⠀⠀⠉⠁⠀⠀⠀⠀⢿⣿⣿⣿ 68 | ⣿⣿⣿⣿⣿⣿⣿⣿⢾⣿⣷⠀⠀⠀⠀⡠⠤⢄⠀⠀⠀⠠⣿⣿⣷⠀⢸⣿⣿⣿ 69 | ⣿⣿⣿⣿⣿⣿⣿⣿⡀⠉⠀⠀⠀⠀⠀⢄⠀⢀⠀⠀⠀⠀⠉⠉⠁⠀⠀⣿⣿⣿ 70 | ⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿ 71 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿ 72 | "; 73 | let id = mux.focus(); 74 | mux.add_below(cursive_core::views::TextView::new(surprise), id) 75 | .unwrap(); 76 | } 77 | 78 | fn remove_pane(siv: &mut Cursive) { 79 | let mut mux: cursive_core::views::ViewRef = siv.find_name("Mux").unwrap(); 80 | let id = mux.focus(); 81 | mux.remove_id(id).unwrap(); 82 | } 83 | -------------------------------------------------------------------------------- /assets/config.yml: -------------------------------------------------------------------------------- 1 | # Specify a command to be executed 2 | # like `/bin/bash -l`, `ls`, or any other commands 3 | # the default is bash for Linux 4 | # or powershell.exe for Windows 5 | command: ../target/debug/examples/basic 6 | 7 | # Specify the current working directory path 8 | # the default is the current working directory path 9 | cwd: null 10 | 11 | # Export additional ENV variables 12 | env: 13 | recording: true 14 | 15 | # Explicitly set the number of columns 16 | # or use `auto` to take the current 17 | # number of columns of your shell 18 | cols: 80 19 | 20 | # Explicitly set the number of rows 21 | # or use `auto` to take the current 22 | # number of rows of your shell 23 | rows: 24 24 | 25 | # Amount of times to repeat GIF 26 | # If value is -1, play once 27 | # If value is 0, loop indefinitely 28 | # If value is a positive number, loop n times 29 | repeat: 0 30 | 31 | # Quality 32 | # 1 - 100 33 | quality: 100 34 | 35 | # Delay between frames in ms 36 | # If the value is `auto` use the actual recording delays 37 | frameDelay: auto 38 | 39 | # Maximum delay between frames in ms 40 | # Ignored if the `frameDelay` isn't set to `auto` 41 | # Set to `auto` to prevent limiting the max idle time 42 | maxIdleTime: 1000 43 | 44 | # The surrounding frame box 45 | # The `type` can be null, window, floating, or solid` 46 | # To hide the title use the value null 47 | # Don't forget to add a backgroundColor style with a null as type 48 | frameBox: 49 | type: solid 50 | title: null 51 | style: 52 | border: 0px black solid 53 | # boxShadow: none 54 | # margin: 0px 55 | 56 | # Add a watermark image to the rendered gif 57 | # You need to specify an absolute path for 58 | # the image on your machine or a URL, and you can also 59 | # add your own CSS styles 60 | watermark: 61 | imagePath: null 62 | style: 63 | position: absolute 64 | right: 15px 65 | bottom: 15px 66 | width: 100px 67 | opacity: 0.9 68 | 69 | # Cursor style can be one of 70 | # `block`, `underline`, or `bar` 71 | cursorStyle: block 72 | 73 | # Font family 74 | # You can use any font that is installed on your machine 75 | # in CSS-like syntax 76 | fontFamily: "Roboto Mono" 77 | 78 | # The size of the font 79 | fontSize: 12 80 | 81 | # The height of lines 82 | lineHeight: 1 83 | 84 | # The spacing between letters 85 | letterSpacing: 0 86 | 87 | # Theme 88 | theme: 89 | background: "transparent" 90 | foreground: "#afafaf" 91 | cursor: "#c7c7c7" 92 | black: "#232628" 93 | red: "#fc4384" 94 | green: "#b3e33b" 95 | yellow: "#ffa727" 96 | blue: "#003366" 97 | magenta: "#ae89fe" 98 | cyan: "#708387" 99 | white: "#d5d5d0" 100 | brightBlack: "#626566" 101 | brightRed: "#ff7fac" 102 | brightGreen: "#c8ed71" 103 | brightYellow: "#ebdf86" 104 | brightBlue: "#75dff2" 105 | brightMagenta: "#ae89fe" 106 | brightCyan: "#b1c6ca" 107 | brightWhite: "#f9f9f4" 108 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use cursive::{ 2 | views::{LinearLayout, Menubar, NamedView, ResizedView, ScrollView, TextArea}, 3 | Cursive, 4 | }; 5 | use cursive_core::{direction::Orientation, views::TextView}; 6 | use cursive_multiplex::Mux; 7 | 8 | fn main() { 9 | let mut siv = cursive::default(); 10 | // siv.show_debug_console(); 11 | 12 | let text = " 13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec congue porttitor pellentesque. Vestibulum a tellus sagittis, blandit erat ac, finibus eros. Praesent cursus at ligula laoreet congue. Proin vehicula diam mattis metus aliquet aliquam. Nullam finibus tellus id dolor porta venenatis. Cras vestibulum leo sit amet congue ultrices. Phasellus convallis ut enim tincidunt interdum. 14 | 15 | In velit felis, consectetur quis fringilla id, interdum congue metus. Mauris tincidunt, nibh a fermentum posuere, nibh elit auctor lacus, sollicitudin lobortis nisi arcu quis massa. Ut id augue malesuada justo venenatis pellentesque. Donec egestas nec purus sit amet euismod. Integer aliquet sollicitudin ex id viverra. Vivamus porta odio ac volutpat vehicula. Nullam et nunc in erat imperdiet aliquet vel vel sapien. Nulla viverra porttitor nulla, ut efficitur arcu pharetra sit amet. Nunc aliquet, elit non elementum commodo, augue libero pellentesque lacus, ut iaculis nulla ipsum eu turpis. Ut gravida lacus a nunc dictum maximus. Nulla sollicitudin lobortis malesuada. Praesent fermentum eros ac nisl facilisis, non tincidunt ligula pulvinar. 16 | 17 | Cras elementum hendrerit interdum. Proin in diam elit. Maecenas mollis eros id tristique dictum. Nullam euismod scelerisque nibh, et vulputate ipsum consequat vitae. Nunc tempus lacus diam, non fermentum ligula vehicula vel. Nam commodo sodales purus, eu imperdiet orci vulputate eget. Fusce ac quam leo. 18 | 19 | Morbi id velit a nisi convallis malesuada eget a lorem. Integer gravida varius varius. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas pulvinar est diam, sed egestas mauris congue non. Maecenas malesuada augue urna, et efficitur neque pellentesque eu. Donec turpis magna, feugiat non consectetur eget, luctus a metus. Maecenas gravida luctus tempor. 20 | 21 | Integer sit amet eleifend ex. Vivamus aliquam eros et massa pellentesque gravida. Nam ullamcorper in urna eget condimentum. Integer tincidunt cursus purus, non egestas erat ultrices a. Pellentesque id leo tristique, tincidunt nunc nec, iaculis nisl. Etiam sit amet ex vitae nunc facilisis auctor. Mauris ultrices lobortis purus, eget venenatis odio. Donec vulputate arcu nunc, quis posuere eros vestibulum non. Nullam aliquam ex ac mi varius, non sodales enim ultricies. Phasellus nec feugiat enim, at vestibulum enim. Nulla fermentum velit sem, ac dapibus nisi lobortis eu. Nulla eget consectetur massa, sed eleifend lorem. Ut convallis erat nec sapien facilisis posuere. Nam sit amet mollis tortor. Donec posuere neque eu risus sodales, vitae maximus erat sagittis. "; 22 | 23 | let mut mux = Mux::new(); 24 | let node1 = mux 25 | .add_right_of( 26 | ScrollView::new(TextView::new(text)), 27 | mux.root().build().unwrap(), 28 | ) 29 | .expect("text view failed"); 30 | 31 | let mut menubar = Menubar::new(); 32 | menubar.add_leaf("Hello from cursive_multiplex", |_| {}); 33 | menubar.add_leaf("Feel free to try out the examples simply with `cargo run --example=basic` or `cargo run --example=tily`", |_|{}); 34 | 35 | let node2 = mux 36 | .add_right_of(ResizedView::with_full_screen(TextArea::new()), node1) 37 | .unwrap(); 38 | if let Some(textview) = mux.active_view_mut() { 39 | let valid_view = textview 40 | .downcast_mut::>() 41 | .unwrap() 42 | .get_inner_mut(); 43 | valid_view.set_content( 44 | "This text is added by later modification! Check out the `basic` example to see how.", 45 | ); 46 | } 47 | let _ = mux 48 | .add_below(ResizedView::with_full_screen(TextArea::new()), node2) 49 | .unwrap(); 50 | 51 | mux.set_container_split_ratio(node2, 0.7).unwrap(); 52 | 53 | let idlayer = NamedView::new("Mux", mux); 54 | let mut linear = LinearLayout::new(Orientation::Vertical); 55 | 56 | linear.add_child(idlayer); 57 | linear.add_child(menubar); 58 | siv.add_fullscreen_layer(linear); 59 | siv.add_global_callback('q', Cursive::quit); 60 | siv.add_global_callback( 61 | cursive::event::Event::Alt(cursive_core::event::Key::Ins), 62 | move |ref mut siv| { 63 | add_pane(siv); 64 | }, 65 | ); 66 | siv.add_global_callback( 67 | cursive::event::Event::Alt(cursive_core::event::Key::Del), 68 | move |ref mut siv| { 69 | remove_pane(siv); 70 | }, 71 | ); 72 | cursive::logger::init(); 73 | siv.run(); 74 | } 75 | 76 | fn add_pane(siv: &mut Cursive) { 77 | let mut mux: cursive::views::ViewRef = siv.find_name("Mux").unwrap(); 78 | let surprise = "⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⣠⣤⣶⣶ 79 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⢰⣿⣿⣿⣿ 80 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣀⣀⣾⣿⣿⣿⣿ 81 | ⣿⣿⣿⣿⣿⡏⠉⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿ 82 | ⣿⣿⣿⣿⣿⣿⠀⠀⠀⠈⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠉⠁⠀⣿ 83 | ⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠙⠿⠿⠿⠻⠿⠿⠟⠿⠛⠉⠀⠀⠀⠀⠀⣸⣿ 84 | ⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿ 85 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣴⣿⣿⣿⣿ 86 | ⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⢰⣹⡆⠀⠀⠀⠀⠀⠀⣭⣷⠀⠀⠀⠸⣿⣿⣿⣿ 87 | ⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠈⠉⠀⠀⠤⠄⠀⠀⠀⠉⠁⠀⠀⠀⠀⢿⣿⣿⣿ 88 | ⣿⣿⣿⣿⣿⣿⣿⣿⢾⣿⣷⠀⠀⠀⠀⡠⠤⢄⠀⠀⠀⠠⣿⣿⣷⠀⢸⣿⣿⣿ 89 | ⣿⣿⣿⣿⣿⣿⣿⣿⡀⠉⠀⠀⠀⠀⠀⢄⠀⢀⠀⠀⠀⠀⠉⠉⠁⠀⠀⣿⣿⣿ 90 | ⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿ 91 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿ 92 | "; 93 | let id = mux.focus(); 94 | mux.add_below(cursive::views::TextView::new(surprise), id) 95 | .unwrap(); 96 | } 97 | 98 | fn remove_pane(siv: &mut Cursive) { 99 | let mut mux: cursive::views::ViewRef = siv.find_name("Mux").unwrap(); 100 | let id = mux.focus(); 101 | mux.remove_id(id).unwrap(); 102 | } 103 | -------------------------------------------------------------------------------- /tests/geometric.rs: -------------------------------------------------------------------------------- 1 | use cursive_core::event::{Event, Key}; 2 | use cursive_core::traits::View; 3 | use cursive_core::views::{NamedView, TextArea}; 4 | use cursive_multiplex::Mux; 5 | 6 | #[test] 7 | fn test_line_vertical() { 8 | // Vertical test 9 | 10 | let mut siv = cursive::dummy(); 11 | 12 | println!("Vertical Test"); 13 | let mut test_mux = Mux::new(); 14 | let node1 = test_mux 15 | .add_right_of(TextArea::new(), test_mux.root().build().unwrap()) 16 | .expect("first failed"); 17 | let node2 = test_mux.add_below(TextArea::new(), node1).unwrap(); 18 | let node3 = test_mux.add_below(TextArea::new(), node2).unwrap(); 19 | 20 | let id = NamedView::new("mux".to_string(), test_mux); 21 | 22 | siv.add_fullscreen_layer(id); 23 | siv.run(); 24 | 25 | let mut mux: cursive_core::views::ViewRef = siv.find_name("mux").unwrap(); 26 | assert_eq!(node3, mux.focus()); 27 | mux.on_event(Event::Alt(Key::Up)); 28 | assert_eq!(node2, mux.focus()); 29 | mux.on_event(Event::Alt(Key::Down)); 30 | assert_eq!(node3, mux.focus()); 31 | match mux.on_event(Event::Alt(Key::Left)) { 32 | cursive_core::event::EventResult::Ignored => {} 33 | _ => { 34 | assert!(false); 35 | } 36 | } 37 | } 38 | 39 | #[test] 40 | fn test_triangle() { 41 | let mut mux = Mux::new(); 42 | let node1 = mux 43 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 44 | .expect("first failed"); 45 | let mut siv = cursive::dummy(); 46 | 47 | let node2 = mux.add_right_of(TextArea::new(), node1).unwrap(); 48 | let node3 = mux.add_below(TextArea::new(), node2).unwrap(); 49 | 50 | let id = NamedView::new("mux".to_string(), mux); 51 | siv.add_fullscreen_layer(id); 52 | siv.run(); 53 | let mut mux: cursive_core::views::ViewRef = siv.find_name("mux").unwrap(); 54 | 55 | assert_eq!(mux.focus(), node3); 56 | mux.on_event(Event::Alt(Key::Up)); 57 | assert_eq!(mux.focus(), node2); 58 | match mux.on_event(Event::Alt(Key::Left)) { 59 | cursive_core::event::EventResult::Consumed(_) => { 60 | assert_eq!(mux.focus(), node1); 61 | } 62 | cursive_core::event::EventResult::Ignored => { 63 | println!( 64 | "Not to be ignored Event ignored, Focus was at: {}", 65 | mux.focus() 66 | ); 67 | assert!(false); 68 | } 69 | } 70 | } 71 | 72 | #[test] 73 | fn test_diagonal() { 74 | let mut mux = Mux::new(); 75 | let node1 = mux 76 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 77 | .expect("first failed"); 78 | let mut siv = cursive::dummy(); 79 | 80 | let node2 = mux.add_right_of(TextArea::new(), node1).unwrap(); 81 | let _ = mux.add_below(TextArea::new(), node2).unwrap(); 82 | let upper_right_corner = mux.add_right_of(TextArea::new(), node2).unwrap(); 83 | let bottom_left_corner = mux.add_below(TextArea::new(), node1).unwrap(); 84 | let bottom_left_middle = mux 85 | .add_right_of(TextArea::new(), bottom_left_corner) 86 | .unwrap(); 87 | 88 | let id = NamedView::new("mux".to_string(), mux); 89 | siv.add_fullscreen_layer(id); 90 | siv.run(); 91 | let mut mux: cursive_core::views::ViewRef = siv.find_name("mux").unwrap(); 92 | 93 | println!("Moving left..."); 94 | mux.on_event(Event::Alt(Key::Left)); 95 | assert_eq!(mux.focus(), bottom_left_corner); 96 | println!("Moving right..."); 97 | mux.on_event(Event::Alt(Key::Right)); 98 | assert_eq!(mux.focus(), bottom_left_middle); 99 | println!("Moving up..."); 100 | mux.on_event(Event::Alt(Key::Up)); 101 | assert_eq!(mux.focus(), node1); 102 | println!("Moving right..."); 103 | mux.on_event(Event::Alt(Key::Right)); 104 | assert_eq!(mux.focus(), node2); 105 | println!("Moving right..."); 106 | mux.on_event(Event::Alt(Key::Right)); 107 | assert_eq!(mux.focus(), upper_right_corner); 108 | } 109 | 110 | #[test] 111 | fn test_quadratic() { 112 | // Quadratic test 113 | 114 | let mut siv = cursive::dummy(); 115 | let mut mux = Mux::new(); 116 | let top_left_corner = mux 117 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 118 | .expect("top left corner failed"); 119 | 120 | let top_right_mid = mux.add_right_of(TextArea::new(), top_left_corner).unwrap(); 121 | let bottom_right_mid = mux.add_below(TextArea::new(), top_right_mid).unwrap(); 122 | let bottom_right_corner = mux.add_right_of(TextArea::new(), bottom_right_mid).unwrap(); 123 | let bottom_left_corner = mux.add_below(TextArea::new(), top_left_corner).unwrap(); 124 | let top_left_mid = mux.add_right_of(TextArea::new(), top_left_corner).unwrap(); 125 | let bottom_left_mid = mux 126 | .add_right_of(TextArea::new(), bottom_left_corner) 127 | .unwrap(); 128 | let top_right_corner = mux.add_right_of(TextArea::new(), top_right_mid).unwrap(); 129 | 130 | let id = NamedView::new("mux".to_string(), mux); 131 | 132 | siv.add_fullscreen_layer(id); 133 | siv.run(); 134 | 135 | let mut mux: cursive_core::views::ViewRef = siv.find_name("mux").unwrap(); 136 | 137 | println!("Moving left..."); 138 | mux.on_event(Event::Alt(Key::Left)); 139 | println!("Moving left..."); 140 | mux.on_event(Event::Alt(Key::Left)); 141 | assert_eq!(mux.focus(), top_left_mid); 142 | println!("Moving left..."); 143 | mux.on_event(Event::Alt(Key::Left)); 144 | assert_eq!(mux.focus(), top_left_corner); 145 | println!("Moving down"); 146 | mux.on_event(Event::Alt(Key::Down)); 147 | assert_eq!(mux.focus(), bottom_left_corner); 148 | println!("Moving right..."); 149 | mux.on_event(Event::Alt(Key::Right)); 150 | assert_eq!(mux.focus(), bottom_left_mid); 151 | println!("Moving right..."); 152 | mux.on_event(Event::Alt(Key::Right)); 153 | assert_eq!(mux.focus(), bottom_right_mid); 154 | println!("Moving right..."); 155 | mux.on_event(Event::Alt(Key::Right)); 156 | assert_eq!(mux.focus(), bottom_right_corner); 157 | println!("Moving up..."); 158 | mux.on_event(Event::Alt(Key::Up)); 159 | assert_eq!(mux.focus(), top_right_corner); 160 | 161 | println!("Circle completed"); 162 | } 163 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | use crate::{Id, Mux, Orientation}; 2 | 3 | /// Path used to get the id of a specific pane in the mux. 4 | /// self can be directed by calling `.up()`, `.down()`, `.left()` and `.right()` on the instance. 5 | /// To get the final id of a pane `.build()`. 6 | pub struct Path<'a> { 7 | mux: &'a Mux, 8 | cur_id: Option, 9 | } 10 | 11 | #[derive(Debug, PartialEq)] 12 | pub(crate) enum SearchPath { 13 | Left, 14 | Right, 15 | Up, 16 | Down, 17 | } 18 | 19 | impl<'a> Path<'a> { 20 | fn new(mux: &'a Mux, id: Id) -> Self { 21 | Path { 22 | mux, 23 | cur_id: Some(id), 24 | } 25 | } 26 | 27 | /// Finsihing of the path, Option contains the target Id 28 | /// If Option None no Id could be found fitting to the path 29 | /// Consumes the path 30 | /// # Example 31 | /// ``` 32 | /// # use cursive::views::DummyView; 33 | /// # use cursive_multiplex::Mux; 34 | /// let mut mux = Mux::new(); 35 | /// let node1 = mux.add_right_of(DummyView, mux.root().build().unwrap()).unwrap(); 36 | /// mux.add_below(DummyView, node1); 37 | /// let path = mux.root().up().build(); 38 | /// assert_eq!(node1, path.unwrap()); 39 | /// ``` 40 | pub fn build(self) -> Option { 41 | if let Some(node) = self.cur_id { 42 | if self.mux.tree.get(node).unwrap().get().has_view() || node == self.mux.root { 43 | self.cur_id 44 | } else { 45 | None 46 | } 47 | } else { 48 | self.cur_id 49 | } 50 | } 51 | 52 | /// Going up from the current position in the mux 53 | /// Target can be get by calling `.build()` 54 | pub fn up(self) -> Self { 55 | self.next_node(SearchPath::Up, Orientation::Vertical) 56 | } 57 | 58 | /// Going down from the current position in the mux 59 | /// Target can be get by calling `.build()` 60 | pub fn down(self) -> Self { 61 | self.next_node(SearchPath::Down, Orientation::Vertical) 62 | } 63 | 64 | /// Going left from the current position in the mux 65 | /// Target can be get by calling `.build()` 66 | pub fn left(self) -> Self { 67 | self.next_node(SearchPath::Left, Orientation::Horizontal) 68 | } 69 | 70 | /// Going right from the current position in the mux 71 | /// Target can be get by calling `.build()` 72 | pub fn right(self) -> Self { 73 | self.next_node(SearchPath::Right, Orientation::Horizontal) 74 | } 75 | 76 | fn next_node(mut self, direction: SearchPath, orit: Orientation) -> Self { 77 | if let Some(node) = self.cur_id { 78 | // Node can be passed 79 | if node.children(&self.mux.tree).count() > 0 { 80 | if let Some(node_content) = self.mux.tree.get(node) { 81 | match node_content.get().orientation { 82 | _ if node_content.get().orientation == orit => { 83 | if let Some(new) = node.children(&self.mux.tree).nth(match direction { 84 | SearchPath::Up | SearchPath::Left => 0, 85 | SearchPath::Right | SearchPath::Down => 1, 86 | }) { 87 | self.cur_id = Some(new); 88 | } else { 89 | // Invalid Path 90 | self.cur_id = None; 91 | } 92 | } 93 | _ => { 94 | // Invalid Path 95 | println!("ello"); 96 | self.cur_id = None; 97 | } 98 | } 99 | } else { 100 | // State corrupted, should not occur 101 | self.cur_id = None; 102 | } 103 | } 104 | } 105 | self 106 | } 107 | } 108 | 109 | impl Mux { 110 | /// Getter for the initial path to traverse the tree and find a specific Id. 111 | /// Returns a Path which can be traversed. 112 | pub fn root(&self) -> Path { 113 | Path::new(self, self.root) 114 | } 115 | } 116 | 117 | #[cfg(test)] 118 | mod test { 119 | use super::Mux; 120 | use cursive_core::views::DummyView; 121 | 122 | #[test] 123 | fn path_root() { 124 | let mut mux = Mux::new(); 125 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 126 | mux.add_below(DummyView, node1).unwrap(); 127 | let upper_pane = mux.root().build(); 128 | assert!(upper_pane.is_some()); 129 | } 130 | 131 | #[test] 132 | fn path_up() { 133 | let mut mux = Mux::new(); 134 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 135 | mux.add_below(DummyView, node1).unwrap(); 136 | let upper_pane = mux.root().up().build(); 137 | assert!(upper_pane.is_some()); 138 | assert_eq!(node1, upper_pane.unwrap()); 139 | } 140 | 141 | #[test] 142 | fn path_down() { 143 | let mut mux = Mux::new(); 144 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 145 | let node2 = mux.add_below(DummyView, node1).unwrap(); 146 | let lower_pane = mux.root().down().build(); 147 | assert!(lower_pane.is_some()); 148 | assert_eq!(node2, lower_pane.unwrap()); 149 | } 150 | 151 | #[test] 152 | fn path_left() { 153 | let mut mux = Mux::new(); 154 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 155 | mux.add_right_of(DummyView, node1).unwrap(); 156 | let left_pane = mux.root().left().build(); 157 | assert!(left_pane.is_some()); 158 | assert_eq!(node1, left_pane.unwrap()); 159 | } 160 | 161 | #[test] 162 | fn path_right() { 163 | let mut mux = Mux::new(); 164 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 165 | let node2 = mux.add_right_of(DummyView, node1).unwrap(); 166 | let right_pane = mux.root().right().build(); 167 | assert!(right_pane.is_some()); 168 | assert_eq!(node2, right_pane.unwrap()); 169 | } 170 | 171 | #[test] 172 | fn path_invalid() { 173 | let mut mux = Mux::new(); 174 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 175 | let _ = mux.add_right_of(DummyView, node1).unwrap(); 176 | let root_pane = mux.root().up().build(); 177 | assert!(root_pane.is_none()); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | use crate::error::RenderError; 2 | use crate::{AnyCb, Direction, Event, EventResult, Orientation, Printer, Selector, Vec2, View}; 3 | use cursive_core::direction::Absolute; 4 | use cursive_core::view::CannotFocus; 5 | 6 | pub(crate) struct Node { 7 | pub(crate) view: Option>, 8 | pub(crate) orientation: Orientation, 9 | pub(crate) split_ratio_offset: i16, 10 | pub(crate) split_ratio: f32, 11 | total_position: Option, 12 | size: Option, 13 | pub(crate) total_size: Option, 14 | } 15 | 16 | impl Node { 17 | pub(crate) fn new(v: T, orit: Orientation) -> Self 18 | where 19 | T: View, 20 | { 21 | Self { 22 | view: Some(Box::new(v)), 23 | orientation: orit, 24 | split_ratio_offset: 0, 25 | split_ratio: 0.5, 26 | total_position: None, 27 | size: None, 28 | total_size: None, 29 | } 30 | } 31 | 32 | pub(crate) fn click(&self, mp: Vec2) -> bool { 33 | if let Some(pos) = self.total_position { 34 | if let Some(total_size) = self.total_size { 35 | let end_pos = pos + total_size; 36 | if !pos.fits(mp) && end_pos.fits(mp) { 37 | return true; 38 | } 39 | } 40 | } 41 | false 42 | } 43 | 44 | pub(crate) fn move_offset(&mut self, direction: Absolute) -> Result<(), RenderError> { 45 | if let Some(total_size) = self.total_size { 46 | match direction { 47 | Absolute::Left | Absolute::Up => match direction.into() { 48 | Orientation::Horizontal => { 49 | if (total_size.x as f32 * self.split_ratio) as i16 50 | - self.split_ratio_offset.abs() 51 | > 1 52 | || self.split_ratio_offset > 0 53 | { 54 | self.split_ratio_offset -= 1; 55 | Ok(()) 56 | } else { 57 | Err(RenderError::Arithmetic {}) 58 | } 59 | } 60 | Orientation::Vertical => { 61 | if (total_size.y as f32 * self.split_ratio) as i16 62 | - self.split_ratio_offset.abs() 63 | > 1 64 | || self.split_ratio_offset > 0 65 | { 66 | self.split_ratio_offset -= 1; 67 | Ok(()) 68 | } else { 69 | Err(RenderError::Arithmetic {}) 70 | } 71 | } 72 | }, 73 | Absolute::Right | Absolute::Down => match direction.into() { 74 | Orientation::Horizontal => { 75 | if (total_size.x as f32 * (1.0 - self.split_ratio)) as i16 76 | - self.split_ratio_offset.abs() 77 | > 1 78 | || self.split_ratio_offset < 0 79 | { 80 | self.split_ratio_offset += 1; 81 | Ok(()) 82 | } else { 83 | Err(RenderError::Arithmetic {}) 84 | } 85 | } 86 | Orientation::Vertical => { 87 | if (total_size.y as f32 * (1.0 - self.split_ratio)) as i16 88 | - self.split_ratio_offset.abs() 89 | > 1 90 | || self.split_ratio_offset < 0 91 | { 92 | self.split_ratio_offset += 1; 93 | Ok(()) 94 | } else { 95 | Err(RenderError::Arithmetic {}) 96 | } 97 | } 98 | }, 99 | _ => Err(RenderError::Arithmetic {}), 100 | } 101 | } else { 102 | Err(RenderError::Arithmetic {}) 103 | } 104 | } 105 | 106 | pub(crate) fn new_empty(orit: Orientation, split: f32) -> Self { 107 | Self { 108 | view: None, 109 | orientation: orit, 110 | split_ratio_offset: 0, 111 | split_ratio: split, 112 | total_position: None, 113 | size: None, 114 | total_size: None, 115 | } 116 | } 117 | 118 | pub(crate) fn set_pos(&mut self, pos: Vec2) { 119 | if self.view.is_some() { 120 | self.total_position = Some(pos); 121 | } 122 | } 123 | 124 | pub(crate) fn has_view(&self) -> bool { 125 | self.view.is_some() 126 | } 127 | 128 | pub(crate) fn layout_view(&mut self, vec: Vec2) { 129 | if let Some(x) = self.view.as_mut() { 130 | let size = Vec2::min(vec, x.required_size(vec)); 131 | self.size = Some(x.required_size(vec)); 132 | x.layout(size); 133 | } 134 | self.total_size = Some(vec); 135 | } 136 | 137 | pub(crate) fn on_event(&mut self, evt: Event, zoomed: bool) -> EventResult { 138 | if let Some(view) = self.view.as_mut() { 139 | view.on_event(evt.relativized(if zoomed { 140 | Vec2::new(0, 0) 141 | } else { 142 | self.total_position.unwrap_or_else(|| Vec2::new(0, 0)) 143 | })) 144 | } else { 145 | EventResult::Ignored 146 | } 147 | } 148 | 149 | pub(crate) fn draw(&self, printer: &Printer) { 150 | if let Some(ref view) = self.view { 151 | let printer_crop = { 152 | if let Some(size) = self.size { 153 | // cropped_centered is bugged here, panics on valid values 154 | printer.cropped(size) 155 | } else { 156 | printer.clone() 157 | } 158 | }; 159 | view.draw(&printer_crop); 160 | } 161 | } 162 | 163 | pub(crate) fn take_focus(&mut self) -> Result { 164 | if let Some(view) = self.view.as_mut() { 165 | view.take_focus(Direction::none()) 166 | } else { 167 | Err(CannotFocus) 168 | } 169 | } 170 | 171 | pub(crate) fn call_on_any<'a>(&mut self, slct: &Selector, cb: AnyCb<'a>) { 172 | if let Some(view) = self.view.as_mut() { 173 | view.call_on_any(slct, cb); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Welcome to cursive-multiplex 👋

5 |

6 | 7 | stable build 8 | 9 | 10 | nightly build 11 | 12 | 13 | crates.io 14 | 15 | 16 | Docs.rs 17 | 18 | 19 | GitHub 20 | 21 | 22 | 23 | 24 | 25 | PRs Welcome 26 | 27 |
28 | A tmux like multiplexer for 29 | gyscos/cursive 30 | views 31 |

32 | 33 | --- 34 | 35 | > This project is work-in-progress 36 | 37 | This project provides a tiling window manager for [gyscos/cursive](https://github.com/gyscos/cursive) similar to Tmux. You can place any other `cursive` view inside of a `Mux` view to display these views in complex layouts side by side. Watch the demo below to see how it looks. 38 | 39 | ## How does it look like? `demo` [![terminalizer](https://img.shields.io/badge/GIF-terminalizer-blueviolet.svg)](https://github.com/faressoft/terminalizer) 40 | 41 |
42 | Expand to view 43 | Demo GIF 44 |
45 | 46 | ## Usage 47 | 48 | Simply add to your `Cargo.toml` 49 | 50 | ```toml 51 | [dependencies] 52 | cursive-multiplex = "^0.2.0" 53 | ``` 54 | 55 | And then use `Mux::new` to create a new Mux view 56 | 57 | ```rust 58 | let (mut mux, root_node) = cursive_multiplex::Mux::new( 59 | cursive::views::TextView::new("Hello World!".to_string()) 60 | ); 61 | ``` 62 | 63 | > Mux has defaults defined for key bindings. You can change them with the API described in the [docs](https://docs.rs/cursive-multiplex). 64 | 65 |
66 | Expand to see default keybindings 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
ActionKey
Move focus upAlt +
Move focus rightAlt +
Move focus downAlt +
Move focus leftAlt +
Resize upCtrl +
Resize rightCtrl +
Resize downCtrl +
Resize leftCtrl +
ZoomCtrl + x
110 |
111 | 112 | ### Adding views 113 | 114 | You can add views by giving a path or an id to an existing node e.g. 115 | 116 | ```rust 117 | let new_node = mux.add_right_of( 118 | cursive::views::TextView::new("Foo"), 119 | root_node, 120 | ).expect("adding right panel to root failed"); 121 | ``` 122 | 123 | Its also possible to add views by their path. 124 | ```rust 125 | if let Some(sibbling) = mux.root().right().right().down().build() { 126 | let new_node = mux.add_above( 127 | cursive::views::TextView::new("Foo"), 128 | sibbling, 129 | ).expect("adding by path failed"); 130 | } 131 | ``` 132 | 133 | Returned will be a Result Ok contains the new id assigned to the view, or an error in case of failure. 134 | 135 | ### Removing Views 136 | 137 | You can also remove views, by giving the id of the views. 138 | 139 | ```rust 140 | mux.remove_id(new_node)?; 141 | ``` 142 | 143 | On success the id of the removed node is returned. 144 | 145 | ### Switch Views 146 | 147 | If you want to reorder your views you can easily switch them by using 148 | 149 | ```rust 150 | mux.switch_views(new_node, old_node)?; 151 | ``` 152 | 153 | ## Troubleshooting 154 | 155 | If you find any bugs/unexpected behaviour or you have a proposition for future changes open an issue describing the current behaviour and what you expected. 156 | 157 | ## Development [![cargo test](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fdeinstapel.github.io%2Fcursive-multiplex%2Fcargo-test.json)](https://github.com/deinstapel/cursive-multiplex/actions) 158 | 159 | ### Running the tests 160 | 161 | #### Running all test suites 162 | 163 | Just run 164 | 165 | ``` 166 | $ cargo test 167 | ``` 168 | 169 | to execute all available tests. 170 | 171 | #### Investigating failed tests 172 | 173 | In case some test fails with your changes, you can use the `cargo-insta` tool to investigate the test case. 174 | 175 | To install 176 | ``` 177 | $ cargo install cargo-insta 178 | ``` 179 | 180 | and to run the tests and investigate all failing tests interactively. 181 | 182 | ``` 183 | $ cargo insta review 184 | ``` 185 | 186 | Any changes between the expected and received screen will be then displayed. 187 | 188 | #### shields.io endpoints 189 | 190 | [shields.io](https://shields.io) endpoints are generated inside the `./target/shields` folder. They are used in this README. 191 | 192 | ## Authors 193 | 194 | **Fin Christensen** 195 | 196 | > [:octocat: `@fin-ger`](https://github.com/fin-ger) 197 | > [:elephant: `@fin_ger@weirder.earth`](https://weirder.earth/@fin_ger) 198 | > [:bird: `@fin_ger_github`](https://twitter.com/fin_ger_github) 199 | 200 |
201 | 202 | **Johannes Wünsche** 203 | 204 | > [:octocat: `@jwuensche`](https://github.com/jwuensche) 205 | > [:elephant: `@fredowald@mastodon.social`](https://mastodon.social/web/accounts/843376) 206 | > [:bird: `@Fredowald`](https://twitter.com/fredowald) 207 | 208 | ## Show your support 209 | 210 | Give a :star: if this project helped you! 211 | -------------------------------------------------------------------------------- /assets/cursive-multiplex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 33 | 34 | 35 | 67 | 73 | 74 | 76 | 77 | 79 | image/svg+xml 80 | 82 | 83 | 84 | 85 | 86 | 90 | 97 | 98 | 104 | 111 | 118 | 125 | 132 | 141 | 148 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /src/id.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{AddViewError, RemoveViewError, SwitchError}; 2 | use crate::node::Node; 3 | use crate::path::SearchPath; 4 | use crate::{Mux, Orientation, View}; 5 | 6 | /// Identifier for views in binary tree of mux, typically returned after adding a new view to the multiplexer. 7 | pub type Id = indextree::NodeId; 8 | 9 | impl Mux { 10 | /// Removes the given id from the multiplexer, returns an error if not a valid id contained in the tree or the lone root of the tree. 11 | /// When successful the Id of the removed Node is returned. 12 | /// # Example 13 | /// ``` 14 | /// # fn main () { 15 | /// # let mut mux = cursive_multiplex::Mux::new(); 16 | /// # let node1 = mux.add_right_of(cursive::views::DummyView, mux.root().build().unwrap()).unwrap(); 17 | /// let new_node = mux.add_below(cursive::views::DummyView, node1).unwrap(); 18 | /// mux.remove_id(new_node); 19 | /// # } 20 | /// ``` 21 | pub fn remove_id(&mut self, id: Id) -> Result { 22 | let desc: Vec = self.root.descendants(&self.tree).collect(); 23 | if desc.contains(&id) { 24 | let sib_id: Id; 25 | if id.preceding_siblings(&self.tree).count() > 1 { 26 | sib_id = id.preceding_siblings(&self.tree).nth(1).unwrap(); 27 | } else if id.following_siblings(&self.tree).count() > 1 { 28 | sib_id = id.following_siblings(&self.tree).nth(1).unwrap(); 29 | } else { 30 | return Err(RemoveViewError::Generic {}); 31 | } 32 | let parent = id.ancestors(&self.tree).nth(1).unwrap(); 33 | id.detach(&mut self.tree); 34 | self.invalidated = true; 35 | if let Some(anker) = parent.ancestors(&self.tree).nth(1) { 36 | if anker.children(&self.tree).next().unwrap() == parent { 37 | parent.detach(&mut self.tree); 38 | anker.prepend(sib_id, &mut self.tree); 39 | self.focus = sib_id; 40 | Ok(id) 41 | } else { 42 | parent.detach(&mut self.tree); 43 | anker.append(sib_id, &mut self.tree); 44 | self.focus = sib_id; 45 | Ok(id) 46 | } 47 | } else { 48 | self.root = sib_id; 49 | self.focus = sib_id; 50 | Ok(id) 51 | } 52 | } else { 53 | Err(RemoveViewError::InvalidId { id }) 54 | } 55 | } 56 | 57 | /// Add the given view, below the given Id. 58 | /// The new view and the indexed one will share the space previously given to the give Id. 59 | /// When successful `Ok()` will contain the assigned `Id` 60 | /// # Example 61 | /// ``` 62 | /// # extern crate cursive; 63 | /// # fn main () { 64 | /// let mut mux = cursive_multiplex::Mux::new(); 65 | /// let node1 = mux.add_right_of(cursive::views::DummyView, mux.root().build().unwrap()).unwrap(); 66 | /// let new_node = mux.add_below(cursive::views::DummyView, node1).unwrap(); 67 | /// # } 68 | /// ``` 69 | pub fn add_below(&mut self, v: T, id: Id) -> Result 70 | where 71 | T: View, 72 | { 73 | self.add_node_id(v, id, Orientation::Vertical, SearchPath::Down) 74 | } 75 | 76 | /// Add the given view, above the given Id. 77 | /// The new view and the indexed one will share the space previously given to the give Id. 78 | /// When successful `Ok()` will contain the assigned `Id` 79 | /// # Example 80 | /// ``` 81 | /// # extern crate cursive; 82 | /// # fn main () { 83 | /// let mut mux = cursive_multiplex::Mux::new(); 84 | /// let node1 = mux.add_right_of(cursive::views::DummyView, mux.root().build().unwrap()).unwrap(); 85 | /// let new_node = mux.add_above(cursive::views::DummyView, node1).unwrap(); 86 | /// # } 87 | /// ``` 88 | pub fn add_above(&mut self, v: T, id: Id) -> Result 89 | where 90 | T: View, 91 | { 92 | self.add_node_id(v, id, Orientation::Vertical, SearchPath::Up) 93 | } 94 | 95 | /// Add the given view, left of the given Id. 96 | /// The new view and the indexed one will share the space previously given to the give Id. 97 | /// When successful `Ok()` will contain the assigned `Id` 98 | /// # Example 99 | /// ``` 100 | /// # extern crate cursive; 101 | /// # fn main () { 102 | /// let mut mux = cursive_multiplex::Mux::new(); 103 | /// let node1 = mux.add_right_of(cursive::views::DummyView, mux.root().build().unwrap()).unwrap(); 104 | /// let new_node = mux.add_left_of(cursive::views::DummyView, node1).unwrap(); 105 | /// # } 106 | /// ``` 107 | pub fn add_left_of(&mut self, v: T, id: Id) -> Result 108 | where 109 | T: View, 110 | { 111 | self.add_node_id(v, id, Orientation::Horizontal, SearchPath::Left) 112 | } 113 | 114 | /// Add the given view, right of the given Id. 115 | /// The new view and the indexed one will share the space previously given to the give Id. 116 | /// When successful `Ok()` will contain the assigned `Id` 117 | /// # Example 118 | /// ``` 119 | /// # extern crate cursive; 120 | /// # fn main () { 121 | /// let mut mux = cursive_multiplex::Mux::new(); 122 | /// let node1 = mux.add_right_of(cursive::views::DummyView, mux.root().build().unwrap()).unwrap(); 123 | /// let new_node = mux.add_right_of(cursive::views::DummyView, node1).unwrap(); 124 | /// # } 125 | /// ``` 126 | pub fn add_right_of(&mut self, v: T, id: Id) -> Result 127 | where 128 | T: View, 129 | { 130 | self.add_node_id(v, id, Orientation::Horizontal, SearchPath::Right) 131 | } 132 | 133 | /// Sets the dimensions for partitioning two adjacent panes in the same container. 134 | /// ``` 135 | /// # extern crate cursive; 136 | /// # fn main () { 137 | /// let mut mux = cursive_multiplex::Mux::new(); 138 | /// let node1 = mux.add_right_of(cursive::views::DummyView, mux.root().build().unwrap()).unwrap(); 139 | /// let new_node = mux.add_right_of(cursive::views::DummyView, node1).unwrap(); 140 | /// mux.set_container_split_ratio(new_node, 0.3).unwrap(); 141 | /// # } 142 | /// ``` 143 | pub fn set_container_split_ratio>( 144 | &mut self, 145 | id: Id, 146 | input: T, 147 | ) -> Result<(), AddViewError> { 148 | let ratio = input.into().clamp(0.0, 1.0); 149 | if let Some(parent_id) = self 150 | .tree 151 | .get(id) 152 | .ok_or(AddViewError::GenericError {})? 153 | .parent() 154 | { 155 | let parent = self 156 | .tree 157 | .get_mut(parent_id) 158 | .ok_or(AddViewError::GenericError {})? 159 | .get_mut(); 160 | parent.split_ratio = ratio; 161 | return Ok(()); 162 | } 163 | Err(AddViewError::GenericError {}) 164 | } 165 | 166 | fn add_node_id( 167 | &mut self, 168 | v: T, 169 | id: Id, 170 | orientation: Orientation, 171 | direction: SearchPath, 172 | ) -> Result 173 | where 174 | T: View, 175 | { 176 | self.invalidated = true; 177 | let new_node = self.tree.new_node(Node::new(v, Orientation::Horizontal)); 178 | 179 | let mut node_id; 180 | if let Some(parent) = id.ancestors(&self.tree).nth(1) { 181 | node_id = parent; 182 | } else { 183 | node_id = id; 184 | } 185 | 186 | if node_id.children(&self.tree).count() < 2 187 | && !self.tree.get(node_id).unwrap().get().has_view() 188 | { 189 | match direction { 190 | SearchPath::Up | SearchPath::Left => node_id.prepend(new_node, &mut self.tree), 191 | SearchPath::Down | SearchPath::Right => node_id.append(new_node, &mut self.tree), 192 | } 193 | self.tree.get_mut(node_id).unwrap().get_mut().orientation = orientation; 194 | } else { 195 | // First element is node itself, second direct parent 196 | let parent = node_id; 197 | node_id = id; 198 | 199 | let position: SearchPath; 200 | if parent.children(&self.tree).next().unwrap() == node_id { 201 | position = SearchPath::Left; 202 | } else { 203 | position = SearchPath::Right; 204 | } 205 | 206 | node_id.detach(&mut self.tree); 207 | 208 | let new_intermediate = self 209 | .tree 210 | .new_node(Node::new_empty(orientation, self.default_split_ratio)); 211 | match position { 212 | SearchPath::Right | SearchPath::Down => { 213 | parent.append(new_intermediate, &mut self.tree); 214 | } 215 | SearchPath::Left | SearchPath::Up => { 216 | parent.prepend(new_intermediate, &mut self.tree); 217 | } 218 | } 219 | match direction { 220 | SearchPath::Up | SearchPath::Left => { 221 | new_intermediate.append(new_node, &mut self.tree); 222 | new_intermediate.append(node_id, &mut self.tree); 223 | } 224 | SearchPath::Down | SearchPath::Right => { 225 | new_intermediate.append(node_id, &mut self.tree); 226 | new_intermediate.append(new_node, &mut self.tree); 227 | } 228 | } 229 | debug!("Changed order"); 230 | } 231 | 232 | if self 233 | .tree 234 | .get_mut(new_node) 235 | .unwrap() 236 | .get_mut() 237 | .take_focus() 238 | .is_ok() 239 | { 240 | // Here we discard the potential callback from the focused view. 241 | // Ideally we would bubble it up so it can be processed. 242 | self.focus = new_node; 243 | debug!("Changed Focus: {}", new_node); 244 | } 245 | Ok(new_node) 246 | } 247 | 248 | /// Allows for position switching of two views, returns error if ids not in multiplexer. 249 | /// When successful empty `Ok(())` 250 | /// # Example 251 | /// ``` 252 | /// # extern crate cursive; 253 | /// # fn main () { 254 | /// # let mut mux = cursive_multiplex::Mux::new(); 255 | /// # let node1 = mux.add_right_of(cursive::views::DummyView, mux.root().build().unwrap()).unwrap(); 256 | /// let daniel = mux.add_below(cursive::views::DummyView, node1).unwrap(); 257 | /// let the_cooler_daniel = mux.add_below(cursive::views::DummyView, node1).unwrap(); 258 | /// // Oops I wanted the cooler daniel in another spot 259 | /// mux.switch_views(daniel, the_cooler_daniel); 260 | /// # } 261 | /// ``` 262 | pub fn switch_views(&mut self, fst: Id, snd: Id) -> Result<(), SwitchError> { 263 | if let Some(parent1) = fst.ancestors(&self.tree).nth(1) { 264 | if let Some(parent2) = snd.ancestors(&self.tree).nth(1) { 265 | self.invalidated = true; 266 | if parent1.children(&self.tree).next().unwrap() == fst { 267 | if parent2.children(&self.tree).next().unwrap() == snd { 268 | fst.detach(&mut self.tree); 269 | snd.detach(&mut self.tree); 270 | parent1.checked_prepend(snd, &mut self.tree)?; 271 | parent2.checked_prepend(fst, &mut self.tree)?; 272 | Ok(()) 273 | } else { 274 | fst.detach(&mut self.tree); 275 | snd.detach(&mut self.tree); 276 | parent1.checked_prepend(snd, &mut self.tree)?; 277 | parent2.checked_append(fst, &mut self.tree)?; 278 | Ok(()) 279 | } 280 | } else if parent2.children(&self.tree).next().unwrap() == snd { 281 | fst.detach(&mut self.tree); 282 | snd.detach(&mut self.tree); 283 | parent1.checked_append(snd, &mut self.tree)?; 284 | parent2.checked_prepend(fst, &mut self.tree)?; 285 | Ok(()) 286 | } else { 287 | fst.detach(&mut self.tree); 288 | snd.detach(&mut self.tree); 289 | parent1.checked_append(snd, &mut self.tree)?; 290 | parent2.checked_append(fst, &mut self.tree)?; 291 | Ok(()) 292 | } 293 | } else { 294 | Err(SwitchError::NoParent { from: snd, to: fst }) 295 | } 296 | } else { 297 | Err(SwitchError::NoParent { from: fst, to: snd }) 298 | } 299 | } 300 | } 301 | 302 | #[cfg(test)] 303 | mod test { 304 | use super::Mux; 305 | use cursive_core::views::DummyView; 306 | 307 | #[test] 308 | fn left_to_right() { 309 | let mut mux = Mux::new(); 310 | let node1 = mux.add_left_of(DummyView, mux.root).unwrap(); 311 | let node2 = mux.add_left_of(DummyView, node1).unwrap(); 312 | assert!(mux.switch_views(node1, node2).is_ok()); 313 | } 314 | 315 | #[test] 316 | fn right_to_left() { 317 | let mut mux = Mux::new(); 318 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 319 | let node2 = mux.add_left_of(DummyView, node1).unwrap(); 320 | assert!(mux.switch_views(node2, node1).is_ok()); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/actions.rs: -------------------------------------------------------------------------------- 1 | use crate::id::Id; 2 | use crate::path::SearchPath; 3 | use crate::{Absolute, EventResult, Mux, Orientation, Vec2}; 4 | 5 | impl Mux { 6 | // Handler for mouse events 7 | pub(crate) fn clicked_pane(&self, mp: Vec2) -> Option { 8 | if self.zoomed { 9 | return None; 10 | } 11 | for node in self.root.descendants(&self.tree) { 12 | if self.tree.get(node).unwrap().get().click(mp) { 13 | return Some(node); 14 | } 15 | } 16 | None 17 | } 18 | 19 | pub(crate) fn zoom_focus(&mut self) -> EventResult { 20 | self.zoomed = !self.zoomed; 21 | self.invalidated = true; 22 | EventResult::Consumed(None) 23 | } 24 | 25 | pub(crate) fn move_focus(&mut self, direction: Absolute) -> EventResult { 26 | if self.zoomed { 27 | return EventResult::Ignored; 28 | } 29 | let prev_move = self.focus; 30 | match self.move_focus_relative(direction, self.focus, self.focus) { 31 | EventResult::Consumed(any) => { 32 | self.history.push_back((prev_move, self.focus, direction)); 33 | if self.history.len() > self.history_length { 34 | self.history.pop_front(); 35 | } 36 | EventResult::Consumed(any) 37 | } 38 | EventResult::Ignored => EventResult::Ignored, 39 | } 40 | } 41 | 42 | fn move_focus_relative(&mut self, direction: Absolute, node: Id, origin: Id) -> EventResult { 43 | match self.search_focus_path(direction, node.ancestors(&self.tree).nth(1).unwrap(), node) { 44 | Ok((path, turn_point)) => { 45 | // Traverse the path down again 46 | if let Some(focus) = self.traverse_search_path(path, turn_point, direction, origin) 47 | { 48 | if let Ok(result) = self.tree.get_mut(focus).unwrap().get_mut().take_focus() { 49 | self.focus = focus; 50 | EventResult::Consumed(None).and(result) 51 | } else { 52 | // rejected 53 | self.move_focus_relative(direction, focus, origin) 54 | } 55 | } else { 56 | EventResult::Ignored 57 | } 58 | } 59 | Err(_) => EventResult::Ignored, 60 | } 61 | } 62 | 63 | fn traverse_single_node(&self, action: SearchPath, turn_point: Id, cur_node: Id) -> Option { 64 | let left = || -> Option { cur_node.children(&self.tree).next() }; 65 | let right = || -> Option { cur_node.children(&self.tree).last() }; 66 | let up = left; 67 | let down = right; 68 | match self.tree.get(turn_point).unwrap().get().orientation { 69 | Orientation::Horizontal => { 70 | match action { 71 | // Switching Sides for Left & Right 72 | SearchPath::Right 73 | if self.tree.get(cur_node).unwrap().get().orientation 74 | == Orientation::Horizontal => 75 | { 76 | left() 77 | } 78 | SearchPath::Left 79 | if self.tree.get(cur_node).unwrap().get().orientation 80 | == Orientation::Horizontal => 81 | { 82 | right() 83 | } 84 | // Remain for Up & Down 85 | SearchPath::Up 86 | if self.tree.get(cur_node).unwrap().get().orientation 87 | == Orientation::Vertical => 88 | { 89 | up() 90 | } 91 | SearchPath::Down 92 | if self.tree.get(cur_node).unwrap().get().orientation 93 | == Orientation::Vertical => 94 | { 95 | down() 96 | } 97 | _ => None, 98 | } 99 | } 100 | Orientation::Vertical => { 101 | match action { 102 | // Remain for Left & Right 103 | SearchPath::Right 104 | if self.tree.get(cur_node).unwrap().get().orientation 105 | == Orientation::Horizontal => 106 | { 107 | right() 108 | } 109 | SearchPath::Left 110 | if self.tree.get(cur_node).unwrap().get().orientation 111 | == Orientation::Horizontal => 112 | { 113 | left() 114 | } 115 | // Switch for Up & Down 116 | SearchPath::Up 117 | if self.tree.get(cur_node).unwrap().get().orientation 118 | == Orientation::Vertical => 119 | { 120 | down() 121 | } 122 | SearchPath::Down 123 | if self.tree.get(cur_node).unwrap().get().orientation 124 | == Orientation::Vertical => 125 | { 126 | up() 127 | } 128 | _ => None, 129 | } 130 | } 131 | } 132 | } 133 | 134 | fn traverse_search_path( 135 | &self, 136 | mut path: Vec, 137 | turn_point: Id, 138 | direction: Absolute, 139 | origin: Id, 140 | ) -> Option { 141 | let mut cur_node = turn_point; 142 | while let Some(step) = path.pop() { 143 | match self.traverse_single_node(step, turn_point, cur_node) { 144 | Some(node) => { 145 | cur_node = node; 146 | } 147 | None => { 148 | // Truncate remaining path 149 | // cur_node = cur_node.children(&self.tree).next().unwrap(); 150 | break; 151 | } 152 | } 153 | } 154 | 155 | let check = |comp: Absolute, cur_node: &mut Id| -> Result<(), ()> { 156 | if direction == comp { 157 | match cur_node.children(&self.tree).last() { 158 | Some(node) => { 159 | *cur_node = node; 160 | Ok(()) 161 | } 162 | None => Err(()), 163 | } 164 | } else { 165 | match cur_node.children(&self.tree).next() { 166 | Some(node) => { 167 | *cur_node = node; 168 | Ok(()) 169 | } 170 | None => Err(()), 171 | } 172 | } 173 | }; 174 | 175 | // Check if values exist in the history that specify this path 176 | let goal_opt = { 177 | let history = self.history.iter().rev(); 178 | for entry in history { 179 | match entry { 180 | (goal, past_origin, past_direction) 181 | if *past_direction == direction.invert() && origin == *past_origin => 182 | { 183 | return Some(*goal); 184 | } 185 | _ => {} 186 | } 187 | } 188 | None 189 | }; 190 | 191 | if let Some(goal) = goal_opt { 192 | return Some(goal); 193 | } 194 | 195 | // Have to find nearest child here in case path is too short 196 | while !self.tree.get(cur_node).unwrap().get().has_view() { 197 | match self.tree.get(cur_node).unwrap().get().orientation { 198 | Orientation::Horizontal 199 | if direction == Absolute::Left || direction == Absolute::Right => 200 | { 201 | if check(Absolute::Left, &mut cur_node).is_err() { 202 | return None; 203 | } 204 | } 205 | Orientation::Vertical 206 | if direction == Absolute::Up || direction == Absolute::Down => 207 | { 208 | if check(Absolute::Up, &mut cur_node).is_err() { 209 | return None; 210 | } 211 | } 212 | _ => match cur_node.children(&self.tree).next() { 213 | Some(node) => cur_node = node, 214 | None => return None, 215 | }, 216 | } 217 | } 218 | Some(cur_node) 219 | } 220 | 221 | fn search_focus_path( 222 | &self, 223 | direction: Absolute, 224 | nodeid: Id, 225 | fromid: Id, 226 | ) -> Result<(Vec, Id), ()> { 227 | let mut cur_node = Some(nodeid); 228 | let mut from_node = fromid; 229 | let mut path = Vec::new(); 230 | while cur_node.is_some() { 231 | // println!("Current node in search path: {}", cur_node.unwrap()); 232 | // println!("Originating from node: {}", from_node); 233 | match self.tree.get(cur_node.unwrap()).unwrap().get().orientation { 234 | Orientation::Horizontal 235 | if direction == Absolute::Left || direction == Absolute::Right => 236 | { 237 | if cur_node.unwrap().children(&self.tree).next().unwrap() == from_node { 238 | // Originated from left 239 | path.push(SearchPath::Left); 240 | from_node = cur_node.unwrap(); 241 | if direction == Absolute::Left { 242 | cur_node = cur_node.unwrap().ancestors(&self.tree).nth(1); 243 | if cur_node.is_none() { 244 | return Err(()); 245 | } 246 | } else { 247 | cur_node = None; 248 | } 249 | } else { 250 | // Originated from right 251 | path.push(SearchPath::Right); 252 | from_node = cur_node.unwrap(); 253 | if direction == Absolute::Right { 254 | cur_node = cur_node.unwrap().ancestors(&self.tree).nth(1); 255 | if cur_node.is_none() { 256 | return Err(()); 257 | } 258 | } else { 259 | cur_node = None; 260 | } 261 | } 262 | } 263 | Orientation::Vertical 264 | if direction == Absolute::Up || direction == Absolute::Down => 265 | { 266 | if cur_node.unwrap().children(&self.tree).next().unwrap() == from_node { 267 | // Originated from up 268 | path.push(SearchPath::Up); 269 | from_node = cur_node.unwrap(); 270 | if direction == Absolute::Up { 271 | cur_node = cur_node.unwrap().ancestors(&self.tree).nth(1); 272 | if cur_node.is_none() { 273 | return Err(()); 274 | } 275 | } else { 276 | cur_node = None; 277 | } 278 | } else { 279 | // Originated from down 280 | path.push(SearchPath::Down); 281 | from_node = cur_node.unwrap(); 282 | if direction == Absolute::Down { 283 | cur_node = cur_node.unwrap().ancestors(&self.tree).nth(1); 284 | if cur_node.is_none() { 285 | return Err(()); 286 | } 287 | } else { 288 | cur_node = None; 289 | } 290 | } 291 | } 292 | Orientation::Horizontal => { 293 | if cur_node.unwrap().children(&self.tree).next().unwrap() == from_node { 294 | path.push(SearchPath::Left); 295 | from_node = cur_node.unwrap(); 296 | cur_node = cur_node.unwrap().ancestors(&self.tree).nth(1); 297 | } else { 298 | path.push(SearchPath::Right); 299 | from_node = cur_node.unwrap(); 300 | cur_node = cur_node.unwrap().ancestors(&self.tree).nth(1); 301 | } 302 | } 303 | Orientation::Vertical => { 304 | if cur_node.unwrap().children(&self.tree).next().unwrap() == from_node { 305 | path.push(SearchPath::Up); 306 | from_node = cur_node.unwrap(); 307 | cur_node = cur_node.unwrap().ancestors(&self.tree).nth(1); 308 | } else { 309 | path.push(SearchPath::Down); 310 | from_node = cur_node.unwrap(); 311 | cur_node = cur_node.unwrap().ancestors(&self.tree).nth(1); 312 | } 313 | } 314 | } 315 | } 316 | match self.tree.get(from_node).unwrap().get().orientation { 317 | Orientation::Horizontal if direction == Absolute::Up || direction == Absolute::Down => { 318 | Err(()) 319 | } 320 | Orientation::Vertical 321 | if direction == Absolute::Left || direction == Absolute::Right => 322 | { 323 | Err(()) 324 | } 325 | _ => Ok((path, from_node)), 326 | } 327 | } 328 | 329 | pub(crate) fn resize(&mut self, direction: Absolute) -> EventResult { 330 | // TODO: Do not let children be resized to a lower amount then they said they could be 331 | if self.zoomed { 332 | return EventResult::Ignored; 333 | } 334 | let mut parent = self.focus.ancestors(&self.tree).nth(1); 335 | while parent.is_some() { 336 | if let Some(view) = self.tree.get_mut(parent.unwrap()) { 337 | if view.get().orientation == direction.into() { 338 | match view.get_mut().move_offset(direction) { 339 | Ok(()) => { 340 | self.invalidated = true; 341 | return EventResult::Consumed(None); 342 | } 343 | Err(_) => break, 344 | } 345 | } else { 346 | parent = parent.unwrap().ancestors(&self.tree).nth(1); 347 | } 348 | } 349 | } 350 | EventResult::Ignored 351 | } 352 | } 353 | 354 | impl std::convert::From for Orientation { 355 | fn from(direction: Absolute) -> Orientation { 356 | match direction { 357 | Absolute::Up | Absolute::Down => Orientation::Vertical, 358 | Absolute::Left | Absolute::Right => Orientation::Horizontal, 359 | // If no direction default to Horizontal 360 | Absolute::None => Orientation::Horizontal, 361 | } 362 | } 363 | } 364 | 365 | trait Invertable { 366 | fn invert(&self) -> Self; 367 | } 368 | 369 | impl Invertable for Absolute { 370 | fn invert(&self) -> Absolute { 371 | match self { 372 | Absolute::Right => Absolute::Left, 373 | Absolute::Left => Absolute::Right, 374 | Absolute::Up => Absolute::Down, 375 | Absolute::Down => Absolute::Up, 376 | Absolute::None => Absolute::None, 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /tests/end2end.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::{Receiver, Sender}; 2 | use cursive::backends::puppet::observed::ObservedScreen; 3 | use cursive::backends::puppet::Backend; 4 | use cursive::event::{Event, Key}; 5 | use cursive::views::{Panel, ResizedView, TextArea, TextView}; 6 | use cursive::Vec2; 7 | use cursive_multiplex::Mux; 8 | use insta::assert_display_snapshot; 9 | 10 | fn setup_test_environment(cb: F) -> (Receiver, Sender>) 11 | where 12 | F: FnOnce(&mut cursive::Cursive), 13 | { 14 | let backend = Backend::init(Some(Vec2::new(80, 24))); 15 | let frames = backend.stream(); 16 | let input = backend.input(); 17 | let mut siv = cursive::Cursive::new().into_runner(backend); 18 | cb(&mut siv); 19 | input 20 | .send(Some(Event::Refresh)) 21 | .expect("Refresh not accepted, backend not valid"); 22 | siv.step(); 23 | (frames, input) 24 | } 25 | 26 | struct TestCursive { 27 | siv: cursive::CursiveRunner, 28 | frames: Receiver, 29 | input: Sender>, 30 | } 31 | 32 | impl TestCursive { 33 | fn new(cb: F) -> Self 34 | where 35 | F: FnOnce(&mut cursive::Cursive), 36 | { 37 | let backend = Backend::init(Some(Vec2::new(80, 24))); 38 | let frames = backend.stream(); 39 | let input = backend.input(); 40 | let mut siv = cursive::Cursive::new().into_runner(backend); 41 | cb(&mut siv); 42 | input 43 | .send(Some(Event::Refresh)) 44 | .expect("Refresh not accepted, backend not valid"); 45 | siv.step(); 46 | Self { siv, frames, input } 47 | } 48 | fn _call_on(&mut self, cb: F) 49 | where 50 | F: FnOnce(&mut cursive::Cursive), 51 | { 52 | cb(&mut self.siv); 53 | } 54 | 55 | fn input(&mut self, event: Event) { 56 | self.input 57 | .send(Some(event)) 58 | .expect("Refresh not accepted, backend could not react"); 59 | self.step(); 60 | } 61 | 62 | fn step(&mut self) { 63 | self.input 64 | .send(Some(Event::Refresh)) 65 | .expect("Refresh not accepted, backend could not react"); 66 | self.siv.step(); 67 | } 68 | 69 | fn last_screen(&mut self) -> ObservedScreen { 70 | self.frames.try_iter().last().unwrap() 71 | } 72 | } 73 | 74 | #[test] 75 | fn end2end_complex() { 76 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 77 | let mut mux = Mux::new(); 78 | let left1 = mux 79 | .add_right_of( 80 | TextView::new("left 1".to_string()), 81 | mux.root().build().unwrap(), 82 | ) 83 | .expect("left 1 failed"); 84 | let right1 = mux 85 | .add_right_of(TextView::new("right 1"), left1) 86 | .expect("right 1 failed"); 87 | let _right3 = mux 88 | .add_below(TextView::new("right 3"), right1) 89 | .expect("right 3 failed"); 90 | let _right2 = mux 91 | .add_right_of(TextView::new("right 2"), right1) 92 | .expect("right 2 failed"); 93 | 94 | let _left2 = mux 95 | .add_below(TextView::new("left 2"), left1) 96 | .expect("left 2 failed"); 97 | let _left3 = mux 98 | .add_below(TextView::new("left 3"), left1) 99 | .expect("left 3 failed"); 100 | 101 | siv.add_fullscreen_layer(mux); 102 | }); 103 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 104 | } 105 | 106 | #[test] 107 | fn end2end_complex_focus() { 108 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 109 | let mut mux = Mux::new(); 110 | let left1 = mux 111 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 112 | .expect("left 1 failed"); 113 | 114 | let right1 = mux 115 | .add_right_of(TextArea::new(), left1) 116 | .expect("right 1 failed"); 117 | let _right3 = mux 118 | .add_below(TextArea::new(), right1) 119 | .expect("right 3 failed"); 120 | let right2 = mux 121 | .add_right_of(TextArea::new(), right1) 122 | .expect("right 2 failed"); 123 | 124 | let _left2 = mux 125 | .add_below(TextArea::new(), left1) 126 | .expect("left 2 failed"); 127 | let _left3 = mux 128 | .add_below(TextArea::new(), left1) 129 | .expect("left 3 failed"); 130 | mux.set_focus(right2); 131 | 132 | siv.add_fullscreen_layer(mux); 133 | }); 134 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 135 | } 136 | 137 | #[test] 138 | fn end2end_complex_remove() { 139 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 140 | let mut mux = Mux::new(); 141 | let left1 = mux 142 | .add_right_of(TextView::new("left 1"), mux.root().build().unwrap()) 143 | .unwrap(); 144 | 145 | let right1 = mux 146 | .add_right_of(TextView::new("right 1"), left1) 147 | .expect("right 1 failed"); 148 | let right3 = mux 149 | .add_below(TextView::new("right 3"), right1) 150 | .expect("right 3 failed"); 151 | let _right2 = mux 152 | .add_right_of(TextView::new("right 2"), right1) 153 | .expect("right 2 failed"); 154 | 155 | let _left2 = mux 156 | .add_below(TextView::new("left 2"), left1) 157 | .expect("left 2 failed"); 158 | let _left3 = mux 159 | .add_below(TextView::new("left 3"), left1) 160 | .expect("left 3 failed"); 161 | mux.remove_id(right3).expect("remove failed"); 162 | 163 | siv.add_fullscreen_layer(mux); 164 | }); 165 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 166 | } 167 | 168 | #[test] 169 | fn end2end_complex_resize() { 170 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 171 | let mut mux = Mux::new(); 172 | let left1 = mux 173 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 174 | .expect("left 1 failed"); 175 | 176 | let right1 = mux 177 | .add_right_of(TextArea::new(), left1) 178 | .expect("right 1 failed"); 179 | let _right3 = mux 180 | .add_below(TextArea::new(), right1) 181 | .expect("right 3 failed"); 182 | let _right2 = mux 183 | .add_right_of(TextArea::new(), right1) 184 | .expect("right 2 failed"); 185 | 186 | let _left2 = mux 187 | .add_below(TextArea::new(), left1) 188 | .expect("left 2 failed"); 189 | let _left3 = mux 190 | .add_below(TextArea::new(), left1) 191 | .expect("left 3 failed"); 192 | mux.set_focus(right1); 193 | 194 | siv.add_fullscreen_layer(mux); 195 | }); 196 | tsiv.input(Event::Ctrl(Key::Down)); 197 | tsiv.input(Event::Ctrl(Key::Right)); 198 | tsiv.input(Event::Alt(Key::Left)); 199 | tsiv.input(Event::Alt(Key::Down)); 200 | tsiv.input(Event::Ctrl(Key::Up)); 201 | tsiv.input(Event::Ctrl(Key::Left)); 202 | assert_display_snapshot!(tsiv.last_screen()); 203 | } 204 | 205 | #[test] 206 | fn end2end_complex_switch() { 207 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 208 | let mut mux = Mux::new(); 209 | let left1 = mux 210 | .add_right_of(TextView::new("left 1"), mux.root().build().unwrap()) 211 | .expect("left 1 failed"); 212 | 213 | let right1 = mux 214 | .add_right_of(TextView::new("right 1"), left1) 215 | .expect("right 1 failed"); 216 | let _right3 = mux 217 | .add_below(TextView::new("right 3"), right1) 218 | .expect("right 3 failed"); 219 | let _right2 = mux 220 | .add_right_of(TextView::new("right 2"), right1) 221 | .expect("right 2 failed"); 222 | 223 | let _left2 = mux 224 | .add_below(TextView::new("left 2"), left1) 225 | .expect("left 2 failed"); 226 | let left3 = mux 227 | .add_below(TextView::new("left 3"), left1) 228 | .expect("left 3 failed"); 229 | mux.switch_views(right1, left3).expect("switch failed"); 230 | siv.add_fullscreen_layer(mux); 231 | }); 232 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 233 | } 234 | 235 | #[test] 236 | fn end2end_down_focus() { 237 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 238 | let mut mux = Mux::new(); 239 | let upper = mux 240 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 241 | .expect("upper failed"); 242 | let _lower = mux.add_below(TextArea::new(), upper).expect("lower failed"); 243 | let _id = mux.add_below(TextArea::new(), upper).expect("1st failed"); 244 | mux.set_focus(upper); 245 | siv.add_fullscreen_layer(mux); 246 | }); 247 | tsiv.input(Event::Alt(Key::Down)); 248 | assert_display_snapshot!("down once", tsiv.last_screen()); 249 | tsiv.input(Event::Alt(Key::Down)); 250 | assert_display_snapshot!("down twice", tsiv.last_screen()); 251 | tsiv.input(Event::Alt(Key::Down)); 252 | assert_display_snapshot!("down thrice", tsiv.last_screen()); 253 | } 254 | 255 | #[test] 256 | fn end2end_horizontal() { 257 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 258 | let mut mux = Mux::new(); 259 | let root = mux 260 | .add_right_of(TextView::new("Center"), mux.root().build().unwrap()) 261 | .expect("Center failed"); 262 | let _id = mux 263 | .add_right_of(TextView::new("Right"), root) 264 | .expect("Right failed"); 265 | let _id = mux 266 | .add_left_of(TextView::new("Left"), root) 267 | .expect("Left failed"); 268 | siv.add_fullscreen_layer(mux); 269 | }); 270 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 271 | } 272 | 273 | #[test] 274 | fn end2end_horizontal_fixed_size() { 275 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 276 | let mut mux = Mux::new(); 277 | let root = mux 278 | .add_right_of(TextView::new("Root"), mux.root().build().unwrap()) 279 | .expect("Center failed"); 280 | let view = ResizedView::with_fixed_size((42, 11), Panel::new(TextView::new("Fixed"))); 281 | let _id = mux.add_right_of(view, root).expect("Fixed failed"); 282 | 283 | siv.add_fullscreen_layer(mux); 284 | }); 285 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 286 | } 287 | 288 | #[test] 289 | fn end2end_horizontal_remove() { 290 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 291 | let mut mux = Mux::new(); 292 | let root = mux 293 | .add_right_of(TextView::new("Center"), mux.root().build().unwrap()) 294 | .expect("Center failed"); 295 | let _id = mux 296 | .add_left_of(TextView::new("Left"), root) 297 | .expect("Left failed"); 298 | let _id = mux 299 | .add_right_of(TextView::new("Right"), root) 300 | .expect("Right failed"); 301 | mux.remove_id(root).expect("remove failed"); 302 | siv.add_fullscreen_layer(mux); 303 | }); 304 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 305 | } 306 | 307 | #[test] 308 | fn end2end_down_resize() { 309 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 310 | let mut mux = Mux::new(); 311 | let left = mux 312 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 313 | .expect("left failed"); 314 | let center = mux 315 | .add_right_of(TextArea::new(), left) 316 | .expect("Center failed"); 317 | let _right = mux 318 | .add_right_of(TextArea::new(), center) 319 | .expect("Right failed"); 320 | mux.set_focus(left); 321 | siv.add_fullscreen_layer(mux); 322 | }); 323 | tsiv.input(Event::Ctrl(Key::Left)); 324 | tsiv.input(Event::Ctrl(Key::Left)); 325 | tsiv.input(Event::Ctrl(Key::Right)); 326 | assert_display_snapshot!(tsiv.last_screen()); 327 | } 328 | 329 | #[test] 330 | fn end2end_horizontal_switch() { 331 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 332 | let mut mux = Mux::new(); 333 | let right = mux 334 | .add_right_of( 335 | TextView::new("Right".to_string()), 336 | mux.root().build().unwrap(), 337 | ) 338 | .expect("right failed"); 339 | let left = mux 340 | .add_right_of(TextView::new("Left"), right) 341 | .expect("Left failed"); 342 | mux.switch_views(right, left).expect("switch failed"); 343 | siv.add_fullscreen_layer(mux); 344 | }); 345 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 346 | } 347 | 348 | #[test] 349 | fn end2end_left_focus() { 350 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 351 | let mut mux = Mux::new(); 352 | let left = mux 353 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 354 | .expect("left failed"); 355 | let right = mux 356 | .add_right_of(TextArea::new(), left) 357 | .expect("right failed"); 358 | let _id = mux.add_right_of(TextArea::new(), left).expect("1st failed"); 359 | mux.set_focus(right); 360 | siv.add_fullscreen_layer(mux); 361 | }); 362 | tsiv.input(Event::Alt(Key::Left)); 363 | assert_display_snapshot!("left once", tsiv.last_screen()); 364 | tsiv.input(Event::Alt(Key::Left)); 365 | assert_display_snapshot!("left twice", tsiv.last_screen()); 366 | tsiv.input(Event::Alt(Key::Left)); 367 | assert_display_snapshot!("left thrice", tsiv.last_screen()); 368 | } 369 | 370 | #[test] 371 | fn end2end_right_focus() { 372 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 373 | let mut mux = Mux::new(); 374 | let left = mux 375 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 376 | .expect("left failed"); 377 | let _right = mux 378 | .add_right_of(TextArea::new(), left) 379 | .expect("right failed"); 380 | let _id = mux.add_right_of(TextArea::new(), left).expect("1st failed"); 381 | mux.set_focus(left); 382 | siv.add_fullscreen_layer(mux); 383 | }); 384 | tsiv.input(Event::Alt(Key::Right)); 385 | assert_display_snapshot!("right once", tsiv.last_screen()); 386 | tsiv.input(Event::Alt(Key::Right)); 387 | assert_display_snapshot!("right twice", tsiv.last_screen()); 388 | tsiv.input(Event::Alt(Key::Right)); 389 | assert_display_snapshot!("right thrice", tsiv.last_screen()); 390 | } 391 | 392 | #[test] 393 | fn end2end_smoke() { 394 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 395 | let mut mux = Mux::new(); 396 | let _node1 = mux 397 | .add_right_of( 398 | TextView::new("Hello World".to_string()), 399 | mux.root().build().unwrap(), 400 | ) 401 | .expect("hello failed"); 402 | let boxview = ResizedView::with_fixed_size((42, 11), mux); 403 | siv.add_layer(boxview); 404 | }); 405 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 406 | } 407 | 408 | #[test] 409 | fn end2end_up_focus() { 410 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 411 | let mut mux = Mux::new(); 412 | let upper = mux 413 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 414 | .expect("upper failed"); 415 | let lower = mux.add_below(TextArea::new(), upper).expect("lower failed"); 416 | let _id = mux.add_below(TextArea::new(), upper).expect("1st failed"); 417 | mux.set_focus(lower); 418 | 419 | siv.add_fullscreen_layer(mux); 420 | }); 421 | tsiv.input(Event::Alt(Key::Up)); 422 | assert_display_snapshot!("up once", tsiv.last_screen()); 423 | tsiv.input(Event::Alt(Key::Up)); 424 | assert_display_snapshot!("up twice", tsiv.last_screen()); 425 | tsiv.input(Event::Alt(Key::Up)); 426 | assert_display_snapshot!("up thrice", tsiv.last_screen()); 427 | } 428 | 429 | #[test] 430 | fn end2end_zoom() { 431 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 432 | let mut mux = Mux::new().with_zoom(Event::CtrlChar('x')); 433 | let root = mux 434 | .add_right_of( 435 | TextView::new("Center".to_string()), 436 | mux.root().build().unwrap(), 437 | ) 438 | .expect("Center failed"); 439 | let _id = mux 440 | .add_below(TextView::new("Down"), root) 441 | .expect("Down failed"); 442 | let id = mux 443 | .add_above( 444 | TextView::new( 445 | "Ups asd 446 | asd 447 | asd asd 448 | as a 449 | s da 450 | s a 451 | sdasdasdasdasdfasfgarhbah 452 | ga 453 | fa 454 | sdf 455 | asf 456 | 457 | 458 | 459 | 460 | a 461 | sdfa 462 | sdf 463 | ad 464 | fas 465 | f 466 | 467 | 468 | 469 | asdf 470 | 471 | a 472 | as 473 | DAS 474 | D", 475 | ), 476 | root, 477 | ) 478 | .expect("Up failed"); 479 | mux.set_focus(id); 480 | siv.add_fullscreen_layer(mux); 481 | }); 482 | tsiv.input(Event::CtrlChar('x')); 483 | assert_display_snapshot!(tsiv.last_screen()); 484 | } 485 | 486 | #[test] 487 | fn end2end_vertical() { 488 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 489 | let mut mux = Mux::new(); 490 | let root = mux 491 | .add_right_of( 492 | TextView::new("Center".to_string()), 493 | mux.root().build().unwrap(), 494 | ) 495 | .expect("Center failed"); 496 | let _id = mux 497 | .add_below(TextView::new("Down"), root) 498 | .expect("Down failed"); 499 | let _id = mux.add_above(TextView::new("Up"), root).expect("Up failed"); 500 | siv.add_fullscreen_layer(mux); 501 | }); 502 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 503 | } 504 | 505 | #[test] 506 | fn end2end_vertical_fixed_size() { 507 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 508 | let mut mux = Mux::new(); 509 | let root = mux 510 | .add_right_of(TextView::new("Root"), mux.root().build().unwrap()) 511 | .expect("Root failed"); 512 | let view = ResizedView::with_fixed_size((42, 11), Panel::new(TextView::new("Fixed"))); 513 | let _id = mux.add_below(view, root).expect("Fixed failed"); 514 | 515 | siv.add_fullscreen_layer(mux); 516 | }); 517 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 518 | } 519 | 520 | #[test] 521 | fn end2end_vertical_remove() { 522 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 523 | let mut mux = Mux::new(); 524 | let root = mux 525 | .add_right_of(TextView::new("Center"), mux.root().build().unwrap()) 526 | .expect("Center failed"); 527 | let _id = mux 528 | .add_above(TextView::new("Up"), root) 529 | .expect("Upper failed"); 530 | let _id = mux 531 | .add_below(TextView::new("Down"), root) 532 | .expect("Lower failed"); 533 | mux.remove_id(root).expect("remove failed"); 534 | siv.add_fullscreen_layer(mux); 535 | }); 536 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 537 | } 538 | 539 | #[test] 540 | fn end2end_vertical_resize() { 541 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 542 | let mut mux = Mux::new(); 543 | let up = mux 544 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 545 | .expect("Up failed"); 546 | let center = mux.add_below(TextArea::new(), up).expect("Center failed"); 547 | let _down = mux.add_below(TextArea::new(), center).expect("Down failed"); 548 | mux.set_focus(up); 549 | siv.add_fullscreen_layer(mux); 550 | }); 551 | tsiv.input(Event::Ctrl(Key::Up)); 552 | tsiv.input(Event::Ctrl(Key::Up)); 553 | tsiv.input(Event::Ctrl(Key::Down)); 554 | assert_display_snapshot!(tsiv.last_screen()); 555 | } 556 | 557 | #[test] 558 | fn end2end_switch_views() { 559 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 560 | let mut mux = Mux::new(); 561 | let up = mux 562 | .add_right_of(TextView::new("Up"), mux.root().build().unwrap()) 563 | .expect("Up failed"); 564 | let down = mux 565 | .add_above(TextView::new("Down"), up) 566 | .expect("Left failed"); 567 | mux.switch_views(up, down).expect("switch failed"); 568 | siv.add_fullscreen_layer(mux); 569 | }); 570 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 571 | } 572 | 573 | #[test] 574 | fn end2end_up_down_focus_history() { 575 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 576 | let mut mux = Mux::new(); 577 | let up = mux 578 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 579 | .expect("Up failed"); 580 | let down_left = mux.add_below(TextArea::new(), up).expect("Left failed"); 581 | let _down_right = mux 582 | .add_right_of(TextArea::new(), down_left) 583 | .expect("Right Failed"); 584 | siv.add_fullscreen_layer(mux); 585 | }); 586 | tsiv.input(Event::Alt(Key::Up)); 587 | tsiv.input(Event::Alt(Key::Down)); 588 | assert_display_snapshot!(tsiv.last_screen()); 589 | } 590 | 591 | #[test] 592 | fn end2end_left_right_focus_history() { 593 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 594 | let mut mux = Mux::new(); 595 | let up = mux 596 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 597 | .expect("Up failed"); 598 | let up_right = mux.add_right_of(TextArea::new(), up).expect("Left failed"); 599 | let _down_right = mux 600 | .add_below(TextArea::new(), up_right) 601 | .expect("Right Failed"); 602 | siv.add_fullscreen_layer(mux); 603 | }); 604 | tsiv.input(Event::Alt(Key::Left)); 605 | tsiv.input(Event::Alt(Key::Right)); 606 | assert_display_snapshot!(tsiv.last_screen()); 607 | } 608 | 609 | #[test] 610 | fn end2end_custom_split_ratio_horizontal() { 611 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 612 | let mut mux = Mux::new(); 613 | mux.set_default_split_ratio(0.8); 614 | let first = mux 615 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 616 | .expect("First Insert failed"); 617 | let _ = mux.add_left_of(TextView::new("A very very long text to demonstrate the split that happens later on in this example."), first).expect("Could not add second view."); 618 | siv.add_fullscreen_layer(mux); 619 | }); 620 | assert_display_snapshot!(tsiv.last_screen()); 621 | } 622 | 623 | #[test] 624 | fn end2end_custom_split_ratio_vertical() { 625 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 626 | let mut mux = Mux::new(); 627 | mux.set_default_split_ratio(0.8); 628 | let first = mux 629 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 630 | .expect("First Insert failed"); 631 | let _ = mux.add_below(TextView::new("A very very long text to demonstrate the split that happens later on in this example."), first).expect("Could not add second view."); 632 | siv.add_fullscreen_layer(mux); 633 | }); 634 | assert_display_snapshot!(tsiv.last_screen()); 635 | } 636 | 637 | #[test] 638 | fn end2end_custom_split_ratio_undershoot() { 639 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 640 | let mut mux = Mux::new(); 641 | mux.set_default_split_ratio(-0.2); 642 | let first = mux 643 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 644 | .expect("First Insert failed"); 645 | let _ = mux.add_left_of(TextView::new("A very very long text to demonstrate the split that happens later on in this example."), first).expect("Could not add second view."); 646 | siv.add_fullscreen_layer(mux); 647 | }); 648 | assert_display_snapshot!(tsiv.last_screen()); 649 | } 650 | 651 | #[test] 652 | fn end2end_custom_split_ratio_overshoot() { 653 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 654 | let mut mux = Mux::new(); 655 | mux.set_default_split_ratio(42.0); 656 | let first = mux 657 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 658 | .expect("First Insert failed"); 659 | let _ = mux.add_left_of(TextView::new("A very very long text to demonstrate the split that happens later on in this example."), first).expect("Could not add second view."); 660 | siv.add_fullscreen_layer(mux); 661 | }); 662 | assert_display_snapshot!(tsiv.last_screen()); 663 | } 664 | 665 | #[test] 666 | fn end2end_custom_split_ratio_overshoot_correction() { 667 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 668 | let mut mux = Mux::new(); 669 | mux.set_default_split_ratio(42.0); 670 | let first = mux 671 | .add_right_of(TextArea::new(), mux.root().build().unwrap()) 672 | .expect("First Insert failed"); 673 | mux.set_container_split_ratio(first, 0.5) 674 | .expect("Could not modify id"); 675 | let _ = mux.add_left_of(TextView::new("A very very long text to demonstrate the split that happens later on in this example."), first).expect("Could not add second view."); 676 | siv.add_fullscreen_layer(mux); 677 | }); 678 | assert_display_snapshot!(tsiv.last_screen()); 679 | } 680 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # cursive view multiplexer 2 | //! 3 | //! This crate provides a view for the [cursive tui crate](https://github.com/gyscos/cursive). 4 | //! It provides an easier way to display nesting view structures as for example in tmux in cursive. 5 | //! All that has to be done is to insert the view into cursive and later to operate on the reference of it, to add, remove, switch views. 6 | //! 7 | //! Similar to tmux the user is able to resize, and switch between the current views, given they are focusable. 8 | //! 9 | //! # Usage example 10 | //! ```rust 11 | //! extern crate cursive; 12 | //! extern crate cursive_multiplex; 13 | //! 14 | //! use cursive_multiplex::Mux; 15 | //! use cursive::views::TextView; 16 | //! use cursive::Cursive; 17 | //! 18 | //! fn main() { 19 | //! let mut mux = Mux::new(); 20 | //! let node1 = mux.add_right_of(TextView::new("Hello World"), mux.root().build().unwrap()).unwrap(); 21 | //! let mut siv = cursive::default(); 22 | //! mux.add_right_of(TextView::new("Hello from me too!".to_string()), node1); 23 | //! siv.add_fullscreen_layer(mux); 24 | //! 25 | //! // When your finished setting up 26 | //! // siv.run(); 27 | //! } 28 | //! ``` 29 | 30 | #![doc( 31 | html_logo_url = "https://raw.githubusercontent.com/deinstapel/cursive-multiplex/master/assets/cursive-multiplex.png" 32 | )] 33 | 34 | #[macro_use] 35 | extern crate log; 36 | 37 | mod actions; 38 | mod error; 39 | mod id; 40 | mod node; 41 | mod path; 42 | 43 | use cursive_core::direction::{Absolute, Direction}; 44 | use cursive_core::event::{AnyCb, Event, EventResult, Key, MouseButton, MouseEvent}; 45 | use cursive_core::view::{CannotFocus, Selector, View, ViewNotFound}; 46 | use cursive_core::{Printer, Vec2}; 47 | pub use error::*; 48 | pub use id::Id; 49 | use node::Node; 50 | pub use path::Path; 51 | use std::collections::VecDeque; 52 | use std::convert::TryFrom; 53 | 54 | #[derive(Debug, PartialEq, Clone)] 55 | enum Orientation { 56 | Vertical, 57 | Horizontal, 58 | } 59 | 60 | /// View holding information and managing multiplexer. 61 | pub struct Mux { 62 | tree: indextree::Arena, 63 | root: indextree::NodeId, 64 | default_split_ratio: f32, 65 | focus: indextree::NodeId, 66 | history: VecDeque<(indextree::NodeId, indextree::NodeId, Absolute)>, 67 | history_length: usize, 68 | invalidated: bool, 69 | focus_up: Event, 70 | focus_down: Event, 71 | focus_left: Event, 72 | focus_right: Event, 73 | resize_left: Event, 74 | resize_right: Event, 75 | resize_up: Event, 76 | resize_down: Event, 77 | zoom: Event, 78 | zoomed: bool, 79 | } 80 | 81 | impl View for Mux { 82 | fn draw(&self, printer: &Printer) { 83 | debug!("Current Focus: {}", self.focus); 84 | debug!("Is the current pane focused? {}", self.zoomed); 85 | // println!("Mux currently focused: {}", printer.focused); 86 | if self.zoomed { 87 | if let Some(focused) = self.tree.get(self.focus) { 88 | focused.get().draw(printer); 89 | } 90 | } else { 91 | self.rec_draw(printer, self.root) 92 | } 93 | } 94 | 95 | fn needs_relayout(&self) -> bool { 96 | self.invalidated 97 | } 98 | 99 | fn required_size(&mut self, constraint: Vec2) -> Vec2 { 100 | constraint 101 | } 102 | 103 | fn layout(&mut self, constraint: Vec2) { 104 | self.invalidated = false; 105 | if self.zoomed { 106 | if let Some(focused) = self.tree.get_mut(self.focus) { 107 | focused.get_mut().layout_view(constraint); 108 | } 109 | } else { 110 | self.rec_layout(self.root, constraint, Vec2::zero()); 111 | } 112 | } 113 | 114 | fn take_focus(&mut self, _source: Direction) -> Result { 115 | Ok(EventResult::consumed()) 116 | } 117 | 118 | fn focus_view(&mut self, _: &Selector) -> Result { 119 | Ok(EventResult::consumed()) 120 | } 121 | 122 | fn call_on_any<'a>(&mut self, slct: &Selector, cb: AnyCb<'a>) { 123 | let nodes: Vec = self.root.descendants(&self.tree).collect(); 124 | for node in nodes { 125 | if let Some(node_c) = self.tree.get_mut(node) { 126 | node_c.get_mut().call_on_any(slct, cb); 127 | } 128 | } 129 | } 130 | 131 | fn on_event(&mut self, evt: Event) -> EventResult { 132 | // pre_check if focus has to be changed, we dont want views react to mouse click out of their reach 133 | let mut result = EventResult::Ignored; 134 | if let Event::Mouse { 135 | offset, 136 | position, 137 | event: MouseEvent::Press(MouseButton::Left), 138 | } = evt 139 | { 140 | if let Some(off_pos) = position.checked_sub(offset) { 141 | if let Some(pane) = self.clicked_pane(off_pos) { 142 | if let Ok(res) = self.tree.get_mut(pane).unwrap().get_mut().take_focus() { 143 | if self.focus != pane { 144 | result = res; 145 | self.focus = pane; 146 | self.invalidated = true; 147 | } 148 | } 149 | } 150 | } 151 | } 152 | let result = result.and( 153 | self.tree 154 | .get_mut(self.focus) 155 | .unwrap() 156 | .get_mut() 157 | .on_event(evt.clone(), self.zoomed), 158 | ); 159 | match result { 160 | EventResult::Ignored => match evt { 161 | _ if self.focus_left == evt => self.move_focus(Absolute::Left), 162 | _ if self.focus_right == evt => self.move_focus(Absolute::Right), 163 | _ if self.focus_up == evt => self.move_focus(Absolute::Up), 164 | _ if self.focus_down == evt => self.move_focus(Absolute::Down), 165 | _ if self.resize_left == evt => self.resize(Absolute::Left), 166 | _ if self.resize_right == evt => self.resize(Absolute::Right), 167 | _ if self.resize_up == evt => self.resize(Absolute::Up), 168 | _ if self.resize_down == evt => self.resize(Absolute::Down), 169 | _ if self.zoom == evt => self.zoom_focus(), 170 | _ => EventResult::Ignored, 171 | }, 172 | result => result, 173 | } 174 | } 175 | } 176 | 177 | impl Mux { 178 | /// # Example 179 | /// ``` 180 | /// # extern crate cursive; 181 | /// # fn main () { 182 | /// let mut mux = cursive_multiplex::Mux::new(); 183 | /// # } 184 | /// ``` 185 | pub fn new() -> Self { 186 | let mut new_tree = indextree::Arena::new(); 187 | let new_root = new_tree.new_node(Node::new_empty(Orientation::Horizontal, 0.5)); 188 | Mux { 189 | tree: new_tree, 190 | root: new_root, 191 | default_split_ratio: 0.5, 192 | history: VecDeque::new(), 193 | history_length: 50, 194 | invalidated: true, 195 | focus: new_root, 196 | focus_up: Event::Alt(Key::Up), 197 | focus_down: Event::Alt(Key::Down), 198 | focus_left: Event::Alt(Key::Left), 199 | focus_right: Event::Alt(Key::Right), 200 | resize_left: Event::Ctrl(Key::Left), 201 | resize_right: Event::Ctrl(Key::Right), 202 | resize_up: Event::Ctrl(Key::Up), 203 | resize_down: Event::Ctrl(Key::Down), 204 | zoom: Event::CtrlChar('x'), 205 | zoomed: false, 206 | } 207 | } 208 | 209 | pub fn active_view(&self) -> Option<&dyn View> { 210 | self.tree 211 | .get(self.focus) 212 | .map(|node| node.get()) 213 | .and_then(|node| node.view.as_deref()) 214 | } 215 | 216 | pub fn active_view_mut(&mut self) -> Option<&mut dyn View> { 217 | self.tree 218 | .get_mut(self.focus) 219 | .map(|node| node.get_mut()) 220 | .and_then(|node| node.view.as_deref_mut()) 221 | } 222 | 223 | /// Chainable setter for the default split ratio. 224 | /// Note: Only values from 0 to 1 are valid, if your value exceeds this range it will be clamped. 225 | pub fn with_default_split_ratio>(mut self, split: T) -> Self { 226 | self.default_split_ratio = split.into().clamp(0.0, 1.0); 227 | self.tree.get_mut(self.root).unwrap().get_mut().split_ratio = self.default_split_ratio; 228 | self 229 | } 230 | 231 | /// Setter for the default split ratio. 232 | /// Note: Only values from 0 to 1 are valid, if your value exceeds this range it will be clamped. 233 | pub fn set_default_split_ratio>(&mut self, split: T) { 234 | self.default_split_ratio = split.into().clamp(0.0, 1.0); 235 | self.tree.get_mut(self.root).unwrap().get_mut().split_ratio = self.default_split_ratio; 236 | } 237 | 238 | /// Chainable setter for action 239 | pub fn with_move_focus_up(mut self, evt: Event) -> Self { 240 | self.focus_up = evt; 241 | self 242 | } 243 | /// Chainable setter for action 244 | pub fn with_move_focus_down(mut self, evt: Event) -> Self { 245 | self.focus_down = evt; 246 | self 247 | } 248 | /// Chainable setter for action 249 | pub fn with_move_focus_left(mut self, evt: Event) -> Self { 250 | self.focus_left = evt; 251 | self 252 | } 253 | /// Chainable setter for action 254 | pub fn with_move_focus_right(mut self, evt: Event) -> Self { 255 | self.focus_right = evt; 256 | self 257 | } 258 | /// Chainable setter for action 259 | pub fn with_resize_up(mut self, evt: Event) -> Self { 260 | self.resize_up = evt; 261 | self 262 | } 263 | /// Chainable setter for action 264 | pub fn with_resize_down(mut self, evt: Event) -> Self { 265 | self.resize_down = evt; 266 | self 267 | } 268 | /// Chainable setter for action 269 | pub fn with_resize_left(mut self, evt: Event) -> Self { 270 | self.resize_left = evt; 271 | self 272 | } 273 | /// Chainable setter for action 274 | pub fn with_resize_right(mut self, evt: Event) -> Self { 275 | self.resize_right = evt; 276 | self 277 | } 278 | 279 | /// Chainable setter for action 280 | pub fn with_zoom(mut self, evt: Event) -> Self { 281 | self.zoom = evt; 282 | self 283 | } 284 | 285 | /// Setter for action 286 | pub fn set_move_focus_up(&mut self, evt: Event) { 287 | self.focus_up = evt; 288 | } 289 | /// Setter for action 290 | pub fn set_move_focus_down(&mut self, evt: Event) { 291 | self.focus_down = evt; 292 | } 293 | /// Setter for action 294 | pub fn set_move_focus_left(&mut self, evt: Event) { 295 | self.focus_left = evt; 296 | } 297 | /// Setter for action 298 | pub fn set_move_focus_right(&mut self, evt: Event) { 299 | self.focus_right = evt; 300 | } 301 | /// Setter for action 302 | pub fn set_resize_up(&mut self, evt: Event) { 303 | self.resize_up = evt; 304 | } 305 | /// Setter for action 306 | pub fn set_resize_down(&mut self, evt: Event) { 307 | self.resize_down = evt; 308 | } 309 | /// Setter for action 310 | pub fn set_resize_left(&mut self, evt: Event) { 311 | self.resize_left = evt; 312 | } 313 | /// Setter for action 314 | pub fn set_resize_right(&mut self, evt: Event) { 315 | self.resize_right = evt; 316 | } 317 | 318 | /// Setter for action 319 | pub fn set_zoom(&mut self, evt: Event) { 320 | self.zoom = evt; 321 | } 322 | 323 | /// Chainable setter for the focus the mux should have 324 | pub fn with_focus(mut self, id: Id) -> Self { 325 | let nodes: Vec = self.root.descendants(&self.tree).collect(); 326 | if nodes.contains(&id) { 327 | self.focus = id; 328 | self.invalidated = true; 329 | } 330 | self 331 | } 332 | 333 | /// Setter for the focus the mux should have 334 | pub fn set_focus(&mut self, id: Id) { 335 | let nodes: Vec = self.root.descendants(&self.tree).collect(); 336 | if nodes.contains(&id) { 337 | self.focus = id; 338 | self.invalidated = true; 339 | } 340 | } 341 | 342 | /// Returns the current focused view id. 343 | /// By default the newest node added to the multiplexer gets focused. 344 | /// Focus can also be changed by the user. 345 | /// # Example 346 | /// ``` 347 | /// # extern crate cursive; 348 | /// # fn main () { 349 | /// let mut mux = cursive_multiplex::Mux::new(); 350 | /// let node1 = mux.add_right_of(cursive::views::TextArea::new(), mux.root().build().unwrap()).unwrap(); 351 | /// let current_focus = mux.focus(); 352 | /// assert_eq!(current_focus, node1); 353 | /// # } 354 | /// ``` 355 | pub fn focus(&self) -> Id { 356 | self.focus 357 | } 358 | 359 | fn rec_layout(&mut self, root: Id, constraint: Vec2, start_point: Vec2) { 360 | match root.children(&self.tree).count() { 361 | 1 => self.rec_layout( 362 | root.children(&self.tree).next().unwrap(), 363 | constraint, 364 | start_point, 365 | ), 366 | 2 => { 367 | let left = root.children(&self.tree).next().unwrap(); 368 | let right = root.children(&self.tree).last().unwrap(); 369 | let const1; 370 | let const2; 371 | let root_data = &self.tree.get(root).unwrap().get(); 372 | let orit = root_data.orientation.clone(); 373 | match orit { 374 | Orientation::Horizontal => { 375 | const1 = Vec2::new( 376 | Mux::add_offset( 377 | (constraint.x as f32 * root_data.split_ratio) as usize, 378 | root_data.split_ratio_offset, 379 | ), 380 | constraint.y, 381 | ); 382 | const2 = Vec2::new( 383 | { 384 | let size = Mux::add_offset( 385 | (constraint.x as f32 * root_data.split_ratio) as usize, 386 | -root_data.split_ratio_offset, 387 | ); 388 | if constraint.x % 2 == 0 { 389 | match size.checked_sub(1) { 390 | Some(res) => res, 391 | None => size, 392 | } 393 | } else { 394 | size 395 | } 396 | }, 397 | constraint.y, 398 | ); 399 | } 400 | Orientation::Vertical => { 401 | const1 = Vec2::new( 402 | constraint.x, 403 | Mux::add_offset( 404 | (constraint.y as f32 * root_data.split_ratio) as usize, 405 | root_data.split_ratio_offset, 406 | ), 407 | ); 408 | const2 = Vec2::new(constraint.x, { 409 | let size = Mux::add_offset( 410 | (constraint.y as f32 * root_data.split_ratio) as usize, 411 | -root_data.split_ratio_offset, 412 | ); 413 | if constraint.y % 2 == 0 { 414 | match size.checked_sub(1) { 415 | Some(res) => res, 416 | None => size, 417 | } 418 | } else { 419 | size 420 | } 421 | }); 422 | } 423 | } 424 | self.tree 425 | .get_mut(root) 426 | .unwrap() 427 | .get_mut() 428 | .layout_view(constraint); 429 | self.rec_layout(left, const1, start_point); 430 | self.rec_layout( 431 | right, 432 | const2, 433 | match orit { 434 | Orientation::Vertical => start_point + const1.keep_y(), 435 | Orientation::Horizontal => start_point + const1.keep_x(), 436 | }, 437 | ); 438 | } 439 | 0 => { 440 | self.tree 441 | .get_mut(root) 442 | .unwrap() 443 | .get_mut() 444 | .layout_view(constraint); 445 | self.tree 446 | .get_mut(root) 447 | .unwrap() 448 | .get_mut() 449 | .set_pos(start_point); 450 | } 451 | _ => debug!("Illegal Number of Child Nodes"), 452 | } 453 | } 454 | 455 | fn add_offset(split: usize, offset: i16) -> usize { 456 | if offset < 0 { 457 | match usize::try_from(offset.abs()) { 458 | Ok(u) => { 459 | if split < u { 460 | split 461 | } else { 462 | split - u 463 | } 464 | } 465 | Err(_) => split, 466 | } 467 | } else { 468 | match usize::try_from(offset) { 469 | Ok(u) => split + u, 470 | Err(_) => split, 471 | } 472 | } 473 | } 474 | 475 | fn rec_draw(&self, printer: &Printer, root: Id) { 476 | match root.children(&self.tree).count() { 477 | 1 => self.rec_draw(printer, root.children(&self.tree).next().unwrap()), 478 | 2 => { 479 | debug!("Print Children Nodes"); 480 | let left = root.children(&self.tree).next().unwrap(); 481 | let right = root.children(&self.tree).last().unwrap(); 482 | let printer1; 483 | let printer2; 484 | let root_data = &self.tree.get(root).unwrap().get(); 485 | match root_data.orientation { 486 | Orientation::Horizontal => { 487 | printer1 = printer.cropped(Vec2::new( 488 | Mux::add_offset( 489 | (printer.size.x as f32 * root_data.split_ratio) as usize, 490 | root_data.split_ratio_offset, 491 | ), 492 | printer.size.y, 493 | )); 494 | printer2 = printer 495 | .offset(Vec2::new( 496 | Mux::add_offset( 497 | (printer.size.x as f32 * root_data.split_ratio) as usize, 498 | root_data.split_ratio_offset, 499 | ) + 1, 500 | 0, 501 | )) 502 | .cropped(Vec2::new( 503 | Mux::add_offset( 504 | (printer.size.x as f32 * root_data.split_ratio) as usize, 505 | -root_data.split_ratio_offset, 506 | ), 507 | printer.size.y, 508 | )); 509 | } 510 | Orientation::Vertical => { 511 | printer1 = printer.cropped(Vec2::new( 512 | printer.size.x, 513 | Mux::add_offset( 514 | (printer.size.y as f32 * root_data.split_ratio) as usize, 515 | root_data.split_ratio_offset, 516 | ), 517 | )); 518 | printer2 = printer 519 | .offset(Vec2::new( 520 | 0, 521 | Mux::add_offset( 522 | (printer.size.y as f32 * root_data.split_ratio) as usize, 523 | root_data.split_ratio_offset, 524 | ) + 1, 525 | )) 526 | .cropped(Vec2::new( 527 | printer.size.x, 528 | Mux::add_offset( 529 | (printer.size.y as f32 * root_data.split_ratio) as usize, 530 | -root_data.split_ratio_offset, 531 | ), 532 | )); 533 | } 534 | } 535 | match self.tree.get(root).unwrap().get().orientation { 536 | Orientation::Vertical => { 537 | if printer.size.y > 1 { 538 | printer.print_hline( 539 | Vec2::new( 540 | 0, 541 | Mux::add_offset( 542 | (printer.size.y as f32 * root_data.split_ratio) as usize, 543 | root_data.split_ratio_offset, 544 | ), 545 | ), 546 | printer.size.x, 547 | "─", 548 | ); 549 | } 550 | } 551 | Orientation::Horizontal => { 552 | if printer.size.x > 1 { 553 | printer.print_vline( 554 | Vec2::new( 555 | Mux::add_offset( 556 | (printer.size.x as f32 * root_data.split_ratio) as usize, 557 | root_data.split_ratio_offset, 558 | ), 559 | 0, 560 | ), 561 | printer.size.y, 562 | "│", 563 | ); 564 | } 565 | } 566 | } 567 | self.rec_draw(&printer1, left); 568 | self.rec_draw(&printer2, right); 569 | } 570 | 0 => { 571 | self.tree 572 | .get(root) 573 | .unwrap() 574 | .get() 575 | .draw(&printer.focused(self.focus == root)); 576 | } 577 | _ => debug!("Illegal Number of Child Nodes"), 578 | } 579 | } 580 | } 581 | 582 | impl Default for Mux { 583 | fn default() -> Self { 584 | Mux::new() 585 | } 586 | } 587 | 588 | #[cfg(test)] 589 | mod tree { 590 | use super::Mux; 591 | use cursive_core::event::{Event, EventResult, Key}; 592 | use cursive_core::traits::View; 593 | use cursive_core::views::DummyView; 594 | 595 | #[test] 596 | fn test_remove() { 597 | // General Remove test 598 | let mut test_mux = Mux::new(); 599 | let node1 = test_mux.add_below(DummyView, test_mux.root).unwrap(); 600 | let node2 = test_mux.add_below(DummyView, node1).unwrap(); 601 | let node3 = test_mux.add_below(DummyView, node2).unwrap(); 602 | 603 | print_tree(&test_mux); 604 | test_mux.remove_id(node3).unwrap(); 605 | print_tree(&test_mux); 606 | match test_mux.remove_id(node3) { 607 | Ok(_) => { 608 | print_tree(&test_mux); 609 | println!("Delete should have removed: {}", node3); 610 | assert!(false); 611 | } 612 | Err(_) => {} 613 | } 614 | } 615 | 616 | #[test] 617 | fn test_switch() { 618 | let mut mux = Mux::new(); 619 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 620 | let node2 = mux.add_right_of(DummyView, node1).unwrap(); 621 | let node3 = mux.add_left_of(DummyView, node2).unwrap(); 622 | 623 | mux.switch_views(node1, node3).unwrap(); 624 | } 625 | 626 | #[test] 627 | fn test_zoom() { 628 | let mut mux = Mux::new(); 629 | let node1 = mux.add_right_of(DummyView, mux.root).unwrap(); 630 | let node2 = mux.add_right_of(DummyView, node1).unwrap(); 631 | let _node3 = mux.add_left_of(DummyView, node2).unwrap(); 632 | 633 | match mux.on_event(mux.zoom.clone()) { 634 | EventResult::Consumed(_) => {} 635 | EventResult::Ignored => assert!(false), 636 | } 637 | } 638 | 639 | #[test] 640 | fn test_nesting() { 641 | println!("Nesting Test"); 642 | 643 | let mut mux = Mux::new(); 644 | 645 | let mut nodes = Vec::new(); 646 | 647 | for _ in 0..10 { 648 | print_tree(&mux); 649 | match mux.add_right_of( 650 | DummyView, 651 | if let Some(x) = nodes.last() { 652 | *x 653 | } else { 654 | mux.root 655 | }, 656 | ) { 657 | Ok(node) => { 658 | nodes.push(node); 659 | } 660 | Err(_) => { 661 | assert!(false); 662 | } 663 | } 664 | match mux.add_right_of(DummyView, *nodes.last().unwrap()) { 665 | Ok(node) => { 666 | nodes.push(node); 667 | } 668 | Err(_) => { 669 | assert!(false); 670 | } 671 | } 672 | } 673 | 674 | for node in nodes.iter() { 675 | mux.focus = *node; 676 | direction_test(&mut mux); 677 | } 678 | } 679 | 680 | fn print_tree(mux: &Mux) { 681 | print!("Current Tree: "); 682 | for node in mux.root.descendants(&mux.tree) { 683 | print!("{},", node); 684 | } 685 | println!(); 686 | } 687 | 688 | fn direction_test(mux: &mut Mux) { 689 | // This is a shotgun approach to have a look if any unforeseen focus moves could happen, resulting in a uncertain state 690 | mux.on_event(Event::Key(Key::Up)); 691 | mux.on_event(Event::Key(Key::Left)); 692 | mux.on_event(Event::Key(Key::Down)); 693 | mux.on_event(Event::Key(Key::Right)); 694 | mux.on_event(Event::Key(Key::Up)); 695 | mux.on_event(Event::Key(Key::Left)); 696 | mux.on_event(Event::Key(Key::Left)); 697 | mux.on_event(Event::Key(Key::Down)); 698 | mux.on_event(Event::Key(Key::Right)); 699 | mux.on_event(Event::Key(Key::Up)); 700 | mux.on_event(Event::Key(Key::Left)); 701 | } 702 | } 703 | --------------------------------------------------------------------------------