pax_global_header00006660000000000000000000000064145127576140014526gustar00rootroot0000000000000052 comment=32310f899bd30741b97366dc442bbde2eb8ee7ce lager-0.1.1/000077500000000000000000000000001451275761400126175ustar00rootroot00000000000000lager-0.1.1/.clang-format000066400000000000000000000020661451275761400151760ustar00rootroot00000000000000--- AlignAfterOpenBracket: Align AlignConsecutiveAssignments: 'true' AlignEscapedNewlines: Right AlignTrailingComments: 'true' AllowShortFunctionsOnASingleLine: 'true' AllowShortBlocksOnASingleLine: 'true' AlwaysBreakTemplateDeclarations: 'true' AccessModifierOffset: -4 BinPackArguments: 'false' BinPackParameters: 'false' BreakBeforeBraces: Mozilla BreakBeforeInheritanceComma: 'true' BreakBeforeTernaryOperators: 'true' BreakConstructorInitializers: BeforeComma BreakStringLiterals: 'true' ColumnLimit: '80' CompactNamespaces: 'false' ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' FixNamespaceComments: 'true' IndentCaseLabels: 'false' IndentWidth: '4' IndentWrappedFunctionNames: 'false' KeepEmptyLinesAtTheStartOfBlocks: 'false' Language: Cpp MaxEmptyLinesToKeep: '1' NamespaceIndentation: None PointerAlignment: Left ReflowComments: 'true' SortIncludes: 'true' SortUsingDeclarations: 'true' SpaceAfterCStyleCast: 'true' SpaceAfterTemplateKeyword: 'true' SpaceBeforeAssignmentOperators: 'true' SpaceBeforeParens: ControlStatements TabWidth: '4' UseTab: Never ... lager-0.1.1/.dir-locals.el000066400000000000000000000003151451275761400152470ustar00rootroot00000000000000((nil . ((indent-tabs-mode . nil) (show-trailing-whitespace . t))) (c-mode . ((mode . c++))) (c++-mode . ((eval add-hook 'before-save-hook #'clang-format-buffer nil t)))) lager-0.1.1/.github/000077500000000000000000000000001451275761400141575ustar00rootroot00000000000000lager-0.1.1/.github/FUNDING.yml000066400000000000000000000001251451275761400157720ustar00rootroot00000000000000github: arximboldi patreon: sinusoidal custom: ["paypal.me/sinusoidal", sinusoid.al] lager-0.1.1/.github/workflows/000077500000000000000000000000001451275761400162145ustar00rootroot00000000000000lager-0.1.1/.github/workflows/test.yml000066400000000000000000000057661451275761400177340ustar00rootroot00000000000000name: test on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: cachix/install-nix-action@v22 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v12 with: name: arximboldi signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - run: nix-build macos: runs-on: macos-latest steps: - uses: actions/checkout@v2 - uses: cachix/install-nix-action@v22 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v12 with: name: arximboldi signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - run: nix-shell --run 'mkdir build-mac' - name: configure cmake run: | nix-shell --run " cd build-mac && cmake .. \ -DCMAKE_BUILD_TYPE=Debug " - run: nix-shell --run "cd build-mac && make check -j`nproc`" docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - uses: cachix/install-nix-action@v22 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v12 with: name: arximboldi signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - run: nix-shell --run "mkdir build" - run: nix-shell --run "cd build && cmake .." - run: nix-shell --run "cd build && make docs" - uses: shimataro/ssh-key-action@v2 if: github.ref == 'refs/heads/master' with: key: ${{ secrets.SINUSOIDES_SSH_KEY }} known_hosts: ${{ secrets.SINUSOIDES_KNOWN_HOSTS }} - run: nix-shell --run "cd build && make upload-docs" if: github.ref == 'refs/heads/master' check: strategy: matrix: type: [Debug, Release] compiler: [gcc9] opts: [[]] include: - type: Debug compiler: gcc9 opts: ['coverage'] - type: Debug compiler: clang_10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: cachix/install-nix-action@v22 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v12 with: name: arximboldi signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - run: nix-shell --argstr compiler ${{ matrix.compiler }} --run "mkdir build" - name: configure cmake run: | nix-shell --argstr compiler ${{ matrix.compiler }} --run " cd build && cmake .. \ -DCMAKE_BUILD_TYPE=${{ matrix.type }} \ -DENABLE_COVERAGE=${{ contains(matrix.opts, 'coverage') }} " - run: nix-shell --argstr compiler ${{ matrix.compiler }} --run "cd build && make check -j`nproc`" - run: nix-shell --argstr compiler ${{ matrix.compiler }} --run "bash <(curl -s https://codecov.io/bash)" if: ${{ contains(matrix.opts, 'coverage') }} lager-0.1.1/.gitignore000066400000000000000000000005441451275761400146120ustar00rootroot00000000000000bazel-*/ build/* build-*/* result resources/gui/*.css resources/gui/*.css.map # I am too stupid to now how to make `elm` work properly inside Nix, # so let's distribute this suck that building the package does not # need to call `elm-make` # resources/gui/*.js resources/gui/elm-stuff .sass-cache doc/_build doc/_doxygen tools/travis/ssh-key *.qmlc /.vs lager-0.1.1/.gitmodules000066400000000000000000000002231451275761400147710ustar00rootroot00000000000000[submodule "tools/sinusoidal-sphinx-theme"] path = tools/sinusoidal-sphinx-theme url = https://github.com/arximboldi/sinusoidal-sphinx-theme.git lager-0.1.1/BUILD000066400000000000000000000005661451275761400134100ustar00rootroot00000000000000package(default_visibility = ["//visibility:public"]) cc_library( name = "lager", hdrs = glob([ "lager/**/*.hpp", ]), deps = [ "@boost//:hana", "@boost//:intrusive", "@boost//:intrusive_ptr", "@zug//:zug", "@cereal//:cereal", ], includes = [".", "lager/"], visibility = ["//visibility:public"], ) lager-0.1.1/CMakeLists.txt000066400000000000000000000126421451275761400153640ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.8) cmake_policy(SET CMP0048 NEW) # enable project VERSION cmake_policy(SET CMP0056 NEW) # honor link flags in try_compile() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") project(lager VERSION 0.1.0) if(MSVC) set(CMAKE_CXX_STANDARD 20) add_compile_options(/Wall /Zc:preprocessor /bigobj) else() set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") endif() set(CMAKE_EXPORT_COMPILE_COMMANDS on) set(CMAKE_CXX_EXTENSIONS off) if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Qunused-arguments") endif() include(GNUInstallDirs) option(lager_BUILD_TESTS "Build tests" ON) option(lager_BUILD_FAILURE_TESTS "Build failure tests" ON) option(lager_BUILD_EXAMPLES "Build examples" ON) option(lager_BUILD_DEBUGGER_EXAMPLES "Build examples that showcase the web based debugger" ON) option(lager_BUILD_DOCS "Build docs" ON) option(lager_EMBED_RESOURCES_PATH "Embed installation paths for easier, non-portable resource location" ON) option(lager_DISABLE_STORE_DEPENDENCY_CHECKS "Disable compile-time checks for store dependencies" OFF) if (NOT lager_EMBED_RESOURCES_PATH AND lager_BUILD_EXAMPLES) message(FATAL_ERROR "Examples require embedded resources path") endif() if (MSVC AND NOT lager_DISABLE_STORE_DEPENDENCY_CHECKS) message(WARNING "compile-time checks for store dependencies are currently not supported with msvc") set(lager_DISABLE_STORE_DEPENDENCY_CHECKS ON) endif() if (NOT lager_BUILD_EXAMPLES AND lager_BUILD_DEBUGGER_EXAMPLES) message(WARNING "examples using the web-based debugger are disabled when examples are disabled") set(lager_BUILD_DEBUGGER_EXAMPLES OFF) endif() find_program(CCACHE ccache) if (CCACHE) message(STATUS "Using ccache: ${CCACHE}") set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE}) set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE}) else() message(STATUS "Could not find ccache") endif() # Targets # ======= # the library add_library(lager INTERFACE) target_include_directories(lager INTERFACE $ $ $) if(lager_DISABLE_STORE_DEPENDENCY_CHECKS) message(STATUS "Disabling compile-time checks for store dependencies") target_compile_definitions(lager INTERFACE LAGER_DISABLE_STORE_DEPENDENCY_CHECKS) endif() install(TARGETS lager EXPORT LagerConfig) # requirements for tests and examples if (lager_BUILD_TESTS OR lager_BUILD_EXAMPLES) find_package(Boost 1.56 COMPONENTS system REQUIRED) find_package(Threads REQUIRED) find_package(Immer REQUIRED) find_package(Zug REQUIRED) endif() # the library, local development target if(lager_BUILD_TESTS) find_package(Catch2 REQUIRED) add_library(lager-dev INTERFACE) target_include_directories(lager-dev SYSTEM INTERFACE ${lager_SOURCE_DIR} ${Boost_INCLUDE_DIR} ) target_link_libraries(lager-dev INTERFACE lager ${Boost_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} immer zug ) if (ENABLE_COVERAGE) target_compile_options(lager-dev INTERFACE "--coverage") target_link_libraries(lager-dev INTERFACE "--coverage") endif() if (ENABLE_ASAN) target_compile_options(lager-dev INTERFACE -fno-omit-frame-pointer -fsanitize=address) target_link_options(lager-dev INTERFACE -fsanitize=address) endif() enable_testing() add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Build and run all the tests and examples.") add_subdirectory(test) endif() # the library, with http debugger if (lager_BUILD_EXAMPLES) add_library(lager-example INTERFACE) target_include_directories(lager-example INTERFACE $ $ $ ${Boost_INCLUDE_DIR} ) target_link_libraries(lager-example INTERFACE lager immer ${CMAKE_THREAD_LIBS_INIT} ${Boost_LIBRARIES} ) install(TARGETS lager-example EXPORT LagerConfig) # examples with the http debugger if (lager_BUILD_DEBUGGER_EXAMPLES) add_library(lager-debugger-example INTERFACE) target_link_libraries(lager-debugger-example INTERFACE lager-example Boost::system) if (NOT APPLE) add_custom_target(gui ALL COMMAND make WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/resources/gui" COMMENT "Build debugger web UI") endif() install(TARGETS lager-debugger-example EXPORT LagerConfig) install(FILES resources/gui/gui.js resources/gui/gui.css resources/gui/index.html DESTINATION "${CMAKE_INSTALL_DATADIR}/lager/gui") endif() add_subdirectory(example) endif() if (lager_BUILD_DOCS) add_subdirectory(doc) endif() # Also configure and install the resources_path.hpp file if wanted if (lager_EMBED_RESOURCES_PATH) set(LAGER_PREFIX_PATH ${CMAKE_INSTALL_PREFIX}) configure_file(lager/resources_path.hpp.in ${CMAKE_BINARY_DIR}/include/lager/resources_path.hpp) target_include_directories(lager INTERFACE $) install(FILES ${CMAKE_BINARY_DIR}/include/lager/resources_path.hpp DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/lager") endif() install(EXPORT LagerConfig DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Lager") install(DIRECTORY lager DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" PATTERN "*.hpp") lager-0.1.1/LICENSE000066400000000000000000000021031451275761400136200ustar00rootroot00000000000000MIT License Copyright (c) 2017-present, Juan Pedro Bolivar Puente Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lager-0.1.1/LICENSES/000077500000000000000000000000001451275761400140245ustar00rootroot00000000000000lager-0.1.1/LICENSES/OFL-1.1-RFN.txt000066400000000000000000000076601451275761400161360ustar00rootroot00000000000000SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. lager-0.1.1/README.rst000066400000000000000000000153561451275761400143200ustar00rootroot00000000000000.. image:: https://github.com/arximboldi/lager/workflows/test/badge.svg :target: https://github.com/arximboldi/lager/actions?query=workflow%3Atest+branch%3Amaster :alt: Github Actions Badge .. image:: https://codecov.io/gh/arximboldi/lager/branch/master/graph/badge.svg :target: https://codecov.io/gh/arximboldi/lager :alt: CodeCov Badge .. image:: https://cdn.jsdelivr.net/gh/arximboldi/lager/doc/_static/sinusoidal-badge.svg :target: https://sinusoid.al :alt: Sinusoidal Engineering badge :align: right .. raw:: html Logotype .. include:introduction/start **lager** is a C++ library to assist `value-oriented design`_ by implementing the `unidirectional data-flow architecture`_. It is heavily inspired by Elm_ and Redux_, and enables composable designs by promoting the use of simple value types and testable application logic via pure functions. And you get time-travel for free! .. _unidirectional data-flow architecture: https://www.exclamationlabs.com/blog/the-case-for-unidirectional-data-flow .. _Elm: https://guide.elm-lang.org/architecture .. _Redux: https://redux.js.org/introduction/getting-started .. _value-oriented design: https://www.youtube.com/watch?v=_oBx_NbLghY * **Documentation** (Contents_) * **Code** (GitHub_) * **CppRussia-Piter 2019 Talk**: *Squaring the circle* (`YouTube `_, `Slides `_) * **CppCon 2018 Talk**: *The most valuable values* (`YouTube `_, `Slides `_) * **C++ on Sea 2019 Talk**: *Postmodern immutable data-structures* (`YouTube `_, `Slides `_) .. _contents: https://sinusoid.es/lager/#contents .. _github: https://github.com/arximboldi/lager .. raw:: html This project is part of a long-term vision helping interactive and concurrent C++ programs become easier to write. **Help this project's long term sustainability by becoming a patron or buying a sponsorship package:** juanpe@sinusoid.al .. include:index/end Examples -------- For a guided introductory tour with **code samples**, please read the `architecture overview`_ section. Other examples: .. _architecture overview: https://sinusoid.es/lager/architecture.html * **Counter**, a minimalistic example with multiple UIs (`link `_). * **Autopong**, a basic game using SDL2 (`link `_). * **Ewig**, a terminal text editor with undo, asynchronous loading, and more (`link `_). Why? ---- Most interactive software of the last few decades has been written using an object-oriented interpretation of the `Model View Controller`_ design. This architecture provides nice separation of concerns, allowing the core application logic to be separate from the UI, and a good sense of modularity. However, its reliance on stateful object graphs makes the software hard to test or parallelize. It's reliance on fine-grained callbacks makes composition hard, resulting in subtle problems that are hard to debug. *Value-based unidirectional data-flow* tackles a few of these problems: * Thanks to immutability_ and value-types, it is very easy to add **concurrency** as threads can operate on their local copies of the data without mutexes or other flaky synchronization mechanisms. Instead, worker threads communicate their results back by *dispatching* actions to the main thread. * The application logic is made of `pure functions`_ that can be easily **tested** and are fully reproducible. They interact with the world via special side-effects procedures loosely coupled to the services they need via `dependency injection`_. * This also means that data and call-graphs are always trees or `DAGs`_ (instead of cyclical graphs), with *explicit composition* that is to trace and **debug**. You can also always *snapshot* the state, making undo and time-travel easy peasy! .. _immutability: https://github.com/arximboldi/immer .. _pure functions: https://en.wikipedia.org/wiki/Pure_function .. _model view controller: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller .. _dependency injection: https://en.wikipedia.org/wiki/Dependency_injection .. _DAGs: https://en.wikipedia.org/wiki/Directed_acyclic_graph Dependencies ------------ This library is written in **C++17** and a compliant compiler and standard library necessary. It is `continuously tested`_ with GCC 7, but it might work with other compilers and versions. It also depends on `Zug`_ and `Boost Hana`_. Some optional extensions and modules may have other dependencies documented in their respective sections. .. _Zug: https://github.com/arximboldi/zug/ .. _Boost Hana: https://boostorg.github.io/hana .. _continuously tested: https://travis-ci.org/arximboldi/immer Usage ----- This is a **header only** you can just copy the ``lager`` subfolder somewhere in your *include path*. Some components, like the time-travelling debugger, also require the installation of extra files. You can use `CMake`_ to install the library in your system once you have manually cloned the repository:: mkdir -p build && cd build cmake .. && sudo make install .. _nix package manager: https://nixos.org/nix .. _cmake: https://cmake.org/ Development ----------- In order to develop the library, you will need to compile and run the examples, tests and benchmarks. These require some additional tools. The easiest way to install them is by using the `Nix package manager`_. At the root of the repository just type:: nix-shell This will download all required dependencies and create an isolated environment in which you can use these dependencies, without polluting your system. Then you can proceed to generate a development project using `CMake`_:: mkdir build && cd build cmake .. From then on, one may build and run all tests by doing:: make check License ------- .. image:: https://raw.githubusercontent.com/arximboldi/lager/docs/doc/_static/mit.png :alt: Boost logo :target: https://opensource.org/licenses/MIT :align: right :width: 140 px **This software is licensed under the MIT license**. The full text of the license is can be accessed `via this link `_ and is also included in the ``LICENSE`` file of this software package. lager-0.1.1/WORKSPACE000066400000000000000000000010451451275761400141000ustar00rootroot00000000000000git_repository( name = "com_github_nelhage_rules_boost", commit = "eafab11dbd1d4cd1151f8407bd6ed81d1240d122", remote = "https://github.com/nelhage/rules_boost", ) git_repository( name = "zug", commit = "913fed55a158f7da70ccf4b7f359d056b77c7f7c", remote = "https://github.com/arximboldi/zug", ) git_repository( name = "cereal", commit = "72d3eb200dc0568277255f960bc2bd7eccf8bafc", remote = "https://github.com/USCiLab/cereal", ) load("@com_github_nelhage_rules_boost//:boost/boost.bzl", "boost_deps") boost_deps() lager-0.1.1/cmake/000077500000000000000000000000001451275761400136775ustar00rootroot00000000000000lager-0.1.1/cmake/FindSDL2_ttf.cmake000066400000000000000000000110451451275761400170640ustar00rootroot00000000000000# Locate SDL_ttf library # # This module defines: # # :: # # SDL2_TTF_LIBRARIES, the name of the library to link against # SDL2_TTF_INCLUDE_DIRS, where to find the headers # SDL2_TTF_FOUND, if false, do not try to link against # SDL2_TTF_VERSION_STRING - human-readable string containing the version of SDL_ttf # # # # For backward compatibility the following variables are also set: # # :: # # SDLTTF_LIBRARY (same value as SDL2_TTF_LIBRARIES) # SDLTTF_INCLUDE_DIR (same value as SDL2_TTF_INCLUDE_DIRS) # SDLTTF_FOUND (same value as SDL2_TTF_FOUND) # # # # $SDLDIR is an environment variable that would correspond to the # ./configure --prefix=$SDLDIR used in building SDL. # # Created by Eric Wing. This was influenced by the FindSDL.cmake # module, but with modifications to recognize OS X frameworks and # additional Unix paths (FreeBSD, etc). #============================================================================= # Copyright 2005-2009 Kitware, Inc. # Copyright 2012 Benjamin Eikel # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. find_path(SDL2_TTF_INCLUDE_DIR SDL_ttf.h HINTS ENV SDL2TTFDIR ENV SDL2DIR PATH_SUFFIXES SDL2 # path suffixes to search inside ENV{SDLDIR} include/SDL2 include PATHS ${SDL2_TTF_PATH} ) if (CMAKE_SIZEOF_VOID_P EQUAL 8) set(VC_LIB_PATH_SUFFIX lib/x64) else () set(VC_LIB_PATH_SUFFIX lib/x86) endif () find_library(SDL2_TTF_LIBRARY NAMES SDL2_ttf HINTS ENV SDL2TTFDIR ENV SDL2DIR PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX} PATHS ${SDL2_TTF_PATH} ) if (SDL2_TTF_INCLUDE_DIR AND EXISTS "${SDL2_TTF_INCLUDE_DIR}/SDL_ttf.h") file(STRINGS "${SDL2_TTF_INCLUDE_DIR}/SDL_ttf.h" SDL2_TTF_VERSION_MAJOR_LINE REGEX "^#define[ \t]+SDL_TTF_MAJOR_VERSION[ \t]+[0-9]+$") file(STRINGS "${SDL2_TTF_INCLUDE_DIR}/SDL_ttf.h" SDL2_TTF_VERSION_MINOR_LINE REGEX "^#define[ \t]+SDL_TTF_MINOR_VERSION[ \t]+[0-9]+$") file(STRINGS "${SDL2_TTF_INCLUDE_DIR}/SDL_ttf.h" SDL2_TTF_VERSION_PATCH_LINE REGEX "^#define[ \t]+SDL_TTF_PATCHLEVEL[ \t]+[0-9]+$") string(REGEX REPLACE "^#define[ \t]+SDL_TTF_MAJOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_TTF_VERSION_MAJOR "${SDL2_TTF_VERSION_MAJOR_LINE}") string(REGEX REPLACE "^#define[ \t]+SDL_TTF_MINOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_TTF_VERSION_MINOR "${SDL2_TTF_VERSION_MINOR_LINE}") string(REGEX REPLACE "^#define[ \t]+SDL_TTF_PATCHLEVEL[ \t]+([0-9]+)$" "\\1" SDL2_TTF_VERSION_PATCH "${SDL2_TTF_VERSION_PATCH_LINE}") set(SDL2_TTF_VERSION_STRING ${SDL2_TTF_VERSION_MAJOR}.${SDL2_TTF_VERSION_MINOR}.${SDL2_TTF_VERSION_PATCH}) unset(SDL2_TTF_VERSION_MAJOR_LINE) unset(SDL2_TTF_VERSION_MINOR_LINE) unset(SDL2_TTF_VERSION_PATCH_LINE) unset(SDL2_TTF_VERSION_MAJOR) unset(SDL2_TTF_VERSION_MINOR) unset(SDL2_TTF_VERSION_PATCH) endif () set(SDL2_TTF_LIBRARIES ${SDL2_TTF_LIBRARY}) set(SDL2_TTF_INCLUDE_DIRS ${SDL2_TTF_INCLUDE_DIR}) include(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2_ttf REQUIRED_VARS SDL2_TTF_LIBRARIES SDL2_TTF_INCLUDE_DIRS VERSION_VAR SDL2_TTF_VERSION_STRING) # for backward compatibility set(SDLTTF_LIBRARY ${SDL2_TTF_LIBRARIES}) set(SDLTTF_INCLUDE_DIR ${SDL2_TTF_INCLUDE_DIRS}) set(SDLTTF_FOUND ${SDL2_TTF_FOUND}) lager-0.1.1/cmake/icm_build_failure_testing.cmake000066400000000000000000000207301451275761400220760ustar00rootroot00000000000000# icm_build_failure_testing # # SPDX-License-Identifier: MIT # MIT License: # Copyright (c) 2022 Borislav Stanimirov # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files(the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and / or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions : # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # VERSION HISTORY # # 1.00 (2022-xx-xx) Initial release # # NOTES # This file is bundled with icm_build_failure_parse_and_run.cmake and expects # it to be in the same directory include_guard(GLOBAL) # store current dir to find icm_build_failure_parse_and_run set(ICM_BUILD_FAILURE_TEST_SCRIPT_DIR "${CMAKE_CURRENT_LIST_DIR}") # icm_add_build_failure_test # # Add a build failure test to a project # # Args: # * NAME name - Name of the test (for CTest) # * TARGET target - Optional. # Name of the target executable (whose build will fail). # Defaults to '-test' if not provided # # * SOURCES sources - Sources for the executable # * PARSE src - Optional. One of the sources can be marked as PARSE. If # such is present, it will be parsed for expected errors # # * LIBRARIES libs - Optional. Link libraries for the executable # * LABELS labels - Optional. CTest labels for the test # If none are provided, "build-failure" will be added # * FOLDER folder - Optional. MSVC solution folder for the target # # * ERROR_MATCHES mathes - Optional. Strings with which to match the build # output of the executable # # Notes: # * If neither ERROR_MATCHES nor a PARSE source is present, the test will only # check that the build of the target fails. This is not recommended # * If both are present, the PARSE source takes precedence # * ERROR_MATCHES are a list of strings. The test will check that at least one # of provided strings matches the output of the build # * To list errors in the PARSE source, it must be on a line prepended with: # "// build error:". Example: # # // x.cpp: # // build error: custom error was triggered # static_assert(false, "custom error was triggered") # # If multimple "// build error: lines" are present, at least one of them # needs to match for a successful test. # # Example: # icm_add_build_failure_test( # NAME mylib-bf-foo-func-first-arg-trivial # LIBRARIES mylib # SOURCES # test-helper.cpp # PARSE bf-foo-func-first-arg-trivial.cpp # LABELS bf mylib # ) function(icm_add_build_failure_test) cmake_parse_arguments(ARG "" "NAME;TARGET;FOLDER" "SOURCES;LIBRARIES;ERROR_MATCHES;LABELS" ${ARGN}) if(DEFINED ARG_UNPARSED_ARGUMENTS) message(NOTICE "icm_add_build_failure_test called with unknown arguments") endif() # check sources for a file to parse cmake_parse_arguments(ARG "" "PARSE" "" ${ARG_SOURCES}) set(ARG_SOURCES "${ARG_UNPARSED_ARGUMENTS}") if(NOT DEFINED ARG_TARGET) set(ARG_TARGET "${ARG_NAME}-test") endif() # add an executable # an object library will be sufficient for compilation errors, # but an executable will help with linker errors as well add_executable(${ARG_TARGET} ${ARG_SOURCES}) set_target_properties(${ARG_TARGET} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE ) if(DEFINED ARG_LIBRARIES) target_link_libraries(${ARG_TARGET} PRIVATE ${ARG_LIBRARIES}) endif() if(DEFINED ARG_FOLDER) set_target_properties(${ARG_TARGET} PROPERTIES FOLDER ${ARG_FOLDER}) endif() if(DEFINED ARG_PARSE) # we find error matches from a parsed file # also add the parsed source to the executable's sources target_sources(${ARG_TARGET} PRIVATE "${ARG_PARSE}") # this var is used in the configured file bellow get_filename_component(parsedSourcePath "${ARG_PARSE}" ABSOLUTE) # configure in binary dir based on target name # it's safe as no two targets can have the same name anyway # TODO: when minimal supported version of CMake is 3.18, embed the # helper script [=[here]=] and use file(CONFIGURE) configure_file( "${ICM_BUILD_FAILURE_TEST_SCRIPT_DIR}/icm_build_failure_parse_and_run.cmake" "${CMAKE_BINARY_DIR}/${ARG_TARGET}.cmake" @ONLY ) add_test( NAME ${ARG_NAME} # provide the config as a command line arg here # we cannot configure the file with a generator expression COMMAND ${CMAKE_COMMAND} -DCFG=$ -P ${ARG_TARGET}.cmake WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) else() # we look for error matches in arguments add_test( NAME ${ARG_NAME} COMMAND ${CMAKE_COMMAND} --build . --target ${ARG_TARGET} --config $ WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) if(DEFINED ARG_ERROR_MATCHES) # matches are provided for all test runs set_tests_properties(${ARG_NAME} PROPERTIES PASS_REGULAR_EXPRESSION "${ARG_ERROR_MATCHES}") else() # no matches are provided so just expect the build to fail set_tests_properties(${ARG_NAME} PROPERTIES WILL_FAIL TRUE) endif() endif() if(NOT DEFINED ARG_LABELS) # if labels are not provided, still add "build-failure" set(ARG_LABELS "build-failure") endif() set_tests_properties(${ARG_NAME} PROPERTIES LABELS "${ARG_LABELS}") endfunction() # icm_add_multiple_build_failure_tests # # Add a multiple build failure tests to a project via multiple calls to # icm_add_build_failure_test # # Args: # * SOURCES sources - List of sources to add. Each source will lead to a new # test being added # * PREFIX prefix - Optional. Prefix string to add to each test name # # * LIBRARIES, LABELS, FOLDER, ERROR_MATCHES # forwarded to icm_add_build_failure_test # # Notes: # If ERROR_MATCHES is not present, each source is forwarded as PARSE # function(icm_add_multiple_build_failure_tests) cmake_parse_arguments(ARG "" "PREFIX;FOLDER" "SOURCES;LIBRARIES;ERROR_MATCHES;LABELS" ${ARGN}) if(DEFINED ARG_UNPARSED_ARGUMENTS) message(NOTICE "icm_add_multiple_build_failure_tests called with unknown arguments") endif() foreach(sourceFile ${ARG_SOURCES}) # replace spaces and path separators with '-' string(REGEX REPLACE "[// ]" "-" testName "${sourceFile}") # strip extenstion for target name get_filename_component(testName ${testName} NAME_WLE) if(NOT DEFINED ARG_ERROR_MATCHES) set(sourceFile "PARSE;${sourceFile}") endif() if(DEFINED ARG_PREFIX) set(testName ${ARG_PREFIX}-${testName}) endif() # add test and forward args icm_add_build_failure_test( NAME ${testName} SOURCES "${sourceFile}" LIBRARIES ${ARG_LIBRARIES} LABELS ${ARG_LABELS} ERROR_MATCHES "${ARG_ERROR_MATCHES}" FOLDER ${ARG_FOLDER} ) endforeach() endfunction() # icm_glob_build_failure_tests # # add multiple sources to icm_add_multiple_build_failure_tests via a # file(GLOB pattern) # # Args: # * PATTERN pat - GLOB pattern to use # The rest of the argument are forwarded verbatim to # icm_add_multiple_build_failure_tests function(icm_glob_build_failure_tests) cmake_parse_arguments(ARG "" "PATTERN" "" ${ARGN}) file(GLOB srcs RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} LIST_DIRECTORIES OFF ${ARG_PATTERN} ) icm_add_multiple_build_failure_tests( SOURCES ${srcs} ${ARG_UNPARSED_ARGUMENTS} ) endfunction() lager-0.1.1/codecov.yml000066400000000000000000000000361451275761400147630ustar00rootroot00000000000000ignore: - tools - example lager-0.1.1/default.nix000066400000000000000000000015661451275761400147730ustar00rootroot00000000000000with import {}; let deps = import ./nix/deps.nix {}; in stdenv.mkDerivation rec { name = "lager-git"; version = "git"; src = builtins.filterSource (path: type: baseNameOf path != ".git" && baseNameOf path != "build" && baseNameOf path != "_build" && baseNameOf path != "reports" && baseNameOf path != "tools") ./.; buildInputs = [ ncurses SDL2 SDL2_ttf ]; nativeBuildInputs = [ cmake gcc7 sass pkgconfig ]; cmakeFlags = [ "-Dlager_BUILD_TESTS=OFF" "-Dlager_BUILD_EXAMPLES=OFF" ]; propagatedBuildInputs = [ boost deps.cereal deps.immer deps.zug ]; meta = { homepage = "https://github.com/arximboldi/lager"; description = "library for functional interactive c++ programs"; license = lib.licenses.mit; }; } lager-0.1.1/doc/000077500000000000000000000000001451275761400133645ustar00rootroot00000000000000lager-0.1.1/doc/CMakeLists.txt000066400000000000000000000007741451275761400161340ustar00rootroot00000000000000 # Targets # ======= add_custom_target(doxygen COMMAND doxygen doxygen.config WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") add_custom_target(docs COMMAND make html WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") add_dependencies(docs doxygen) set(lager_ssh_method ssh -p 5488 -o StrictHostKeyChecking=no) add_custom_target(upload-docs COMMAND rsync -av -e \"${lager_ssh_method}\" ${CMAKE_CURRENT_SOURCE_DIR}/_build/html/* raskolnikov@sinusoid.es:public/lager/) lager-0.1.1/doc/Makefile000066400000000000000000000167161451275761400150370ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html bash sphinx-html-hack.bash @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/lager.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/lager.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/lager" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/lager" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." lager-0.1.1/doc/_static/000077500000000000000000000000001451275761400150125ustar00rootroot00000000000000lager-0.1.1/doc/_static/architecture.svg000066400000000000000000000634001451275761400202200ustar00rootroot00000000000000 image/svg+xml ACTION MODEL VIEW update(model, action) → model effects draw(model) render(model) → view dispatch(action) lager-0.1.1/doc/_static/composition.dia000066400000000000000000000035611451275761400200410ustar00rootroot00000000000000]mo6_!_Ynma6t@%6WZH:a}ԋ_ْ,h=wyG>悰n`.f6p> hm;B& $7)٘Aj4_ƱI_ˮ")96!Z㻁K6a0H[e|F7Yd04IzBz}n|NxyQ:b&rȉZ (\޿8~]8ȪRD_z]3qTwL泹5O]]^_7b\rDdcbop{#\):X RS H/]r<5RM|CHrhTV"Gq$#qǥj`'xfZnHE3HZe͆M^lױk""OPlT# 9 4M!n$)'5+xE=, 9* W䩮^*d r23M?rS#m2MLtt )sv=̏qc|H!Wi8 _|{NIR@)Libm{Z!y$f83sUE)J-=㴾hO+qzQH;␾&. pF26 )EgA@x4A}?-ƭi image/svg+xml foo foo_action foo_model update_foo(foo_model,foo_action) bar bar_action bar_model update_bar(bar_model,bar_action) app app_action app_model update_app(app_model,app_action) lager-0.1.1/doc/_static/identity.png000066400000000000000000002131011451275761400173470ustar00rootroot00000000000000PNG  IHDRD*OIzTXtRaw profile type exifxڭYd7rE -c\F3@׹*ln)Ȍx;_RR<_4wϯW>y <:ﻺ>4Yk׾y?|v{Hm8S%;E,6x-֢މ:v篿}{ڟC|P~:v/B\Q?w{gw#"UwS{NBF;ߍ-.{C$77b)XyqE{5ng;kd5s-ݷиS՛ ݫ AlbϨ eN?9i~}gZ})F sc%fj^O=!ޙ#ˡ_c!F~+$!縃Ƭ{sN ؘmDdd54r%rϣXI%RjFj5\K^GZnZon@XW[} n:c8mguXJ+jMﲫm=N8I'riqk7|˭~Ϭ}笅2ϳYS;oAp32S Ur[H)*sʙȑ젌tB7ݯӼſ˜S?2町o1o=|Q]zۭg 1&>k$T5#78߄{=@YFdz'͖L];I= JEyjOїnb;{B(ro7 kzVQԭgpm)uKI|faE5`3oh*}p@pť9@ tnCO苅.eH٘)V w%5%oimչu1px+uIUNV1TMPcT^+(lvX -..ƠhTot߷NDjK7YG2BՍ@jPjC; OB#* C$jAC옖: sO_2'%![hx-ك,IL흅#>QDBMPmwVx`tg{0PQ|-uPy!֝Y;BtMR@4'zi1epErwUnMeSqJ JlXrHekC$Fl `VNΏ[N~Y (X MZoP{oR>T:T9?řʐta/j͢]_ DAy++A4" zɂ&sN tTڼǂ^8l ][H!$ ucҁ~rkaBa75J&1.Gf[8ϢpO#=d%UȇK5`8s Sc${ 8bK% ͳ : B'Qp%e=23dH$.i *mj>fp+f#NGG At2&Qfs=4=5a>I.C۴tfN䡱{è =8GBrGA-CT9-D-An(ݪ@_W)>NƂtZԫ,U#IwJ%H6Zv.!.⫚{ 1ޭ6<2mᠪ5.㈂q{6\ . 5UlcW/zEuÇTm>G$ T`/CȌyA<yuIY{8'f;_h迂@5t( ,S̕Pe.&3 d h.D)CPl iwZQ)8i\:;ح8gǯh=kIiBF Yx0hxus.AAY HfqFp BhL??_I7N |8J5.c$HyYд(1Z,D>#q 8HZQ`DzQ&ҁ*i~vd-J6* =;)YN|Z߀/.s`Q'bآm'!;<;TtFRM(&tyTCaCh D=7:,exGy8|Cɯ^eQ?2׌G~.CC%3k}[tWTF 'φU\3@K 9PzO5J;v* &a5WWLrJBG돚Cc@lUg!dSQf~GS$i= Mı⮲$zZzܤܙCb@I"|%Bq˥`ĝMtU''kޑCa:(|Um-M1 yn8.2E HTOAܡ4ch+$E ġt 1g1p}ڄSz=pK%z1y|z4я%3IƁ~BٻTrbKGD pHYs  tIME + IDATx}[hduW[&2}ӱC VKs{okoy:Nkk/yGkk5\s5\sDkk. r5\s5\skk \s5\s5\@kk"\s5\s5kk5\s5\sDkk. r5\s5\skk \s5\s5\@kk"\s5\s5kk5\s5\sDkk. r5\s5\skk \s5\s5\@kk"\s5\s5kk(wk;j5t:x^ak5מ_h4l6h4f`Hh}fÄktcp͵XRAZE\}F`b1͹ٵ3~ "ڄX,\.+M#EB!t]Z-9NVK}dXe\{Q\F^7[(u?x3. r5&ONN5)MYnx3WV3|~h4$8d2wkI@`H5 |6KV JfS\yB6( } "hFZͰ_{\`"\s@L& lA#o4zi˛@EZUJFC2fµ$#ǑL&(t@$n^a{}"\sm-X,x<>0 /`@'ttE=tttRd$Xl kG?,&0"+ kV9<"b6̀QRY!]5P(B*B2>T*ݨh4Q(8wk\@k2Y8M +$:t`vQ*prr"f r9<mN{<><<45eukfr*=::,EH$-!VV`0i=7},ba4٬mPtxƶwT⢻w\@kNh J!JCNgba@qc~'5֙5 g 2DmJvA~; B!dY;õsm<~㒞S&K$t{h4d2UW䂢zTa?3Gw py^KBCpA96!LA{eBs/VEx.<88@\F,‚4Po<Um->&e$ҁ {5@ pfnRfnpGE xݝNgvajaR P3Yh͘Fu{23-Q䈅qVrTdNv # GCf^wdPe10 A =h+$K$Q@nϰN~. 9ctYD-TvIPqҥ}4\BOq 8M| M4x<uA|>Bc=NBQʌQVP@ bjl6' :>>VoP땖sbbĽ"RRinoGt[j5|gCPjMw/ڤVR\V0ER~!tf'Xx2pza\w^s@H29ԝH0Ll@r|PT*tԫ,Cޝf/Q \lgFVk$nB`FvyQ6lk}UqXz/Lɮ< x`0F\7y!iJ\bhplX̣1x]`dߑHUgA#.:n,G/AM/SeN ;JmL)hDl ׸Ga9 *fwșH$ N{PpeP6t]4Mu[ NmFu1*Jf,..:,X:!u\(T<}9^yvNrqQ.\D"H$eV]xsHfπ"P(+W8`Ʃ>Lb]+c%?S . g&`R(vR^ߞ+|>o8YZeEC2RRPsOCrc[.twNDT V˰dVdd kjg:Ϟ??ZM]cV:~a!E[s!h,vbd2D"캒T*nVP[<ɉ§$ Z`'\ǣNRdXO377jBcLO ::Ci~ um9z~/*5'u>l!ԧR)顂~әFfq=88P#2\ NI ~zjC|"񨃾S# R sڼQT*+J=iw.]PE5 2 zv;kzbʑX,(=X,*00??R2,^rE<Q?nUs>#`aa:o{Fx4277RcRIݏW(NbK$Bh4(Q纙xe2rܹo("vɺ /(]Y}\N*פL3ϕzCL|>b?xތdYF]N鴁5iNŧݔ`V34|Fm"$,Lr@,ع,(<D6.0R}*CWS9 MFI͔RDb$0D5Q;,fDAAV3l6k8 5کA&ͪ4_.sL`P#4)9 x99mRbTaڧnH$VrӾlsFzE֚$I|TIF8rt3]Ƅ u+,:R=9 fBp.5@$SeD1@ )g.Sgd_dDt M EQZ-4 (WV<==U)C:i3azב>Д'b |>r\;uɁ~IZRevX~Nu)2id2qzvvvѣ |2VVVp,//#ڵk9ٖ+O0!}>R'd(gˌ{!YݐSahxtB'''. rtM: v՜rjjH ;VX38?+Bm9.!SNNB곓h4_ k=rpLwkfS}A,g B1 KaVQƣGp]ܻwxkkkx7 jR^]pL' #ƍҗg ݾ}[1FX7 VL2*^(b QE2]8/K\ j,\YX,Bp&"&[96_;ϩb^NDN`E2]k!ӞYG׬90 RG94sB~ Ij5bh ]*?;ƍX__ݻwqUoo-jZ㎧f4UBB~#K@ և.=tGuF*8ieFdmDpp%J&gdI2q)gzpal]-d|7zXZ]N t{)8I0tp=_Co&n޼T*T*NH.]B*}pM|{˗wy^_=ڋv!B+1gG"m:C9guu佻hH'<;jP* `iyMV'qBN ZL8N]^^:,5߉UB63i?)n߾ X[[w3 LZn74\v cuܻwϔ羙Mzh (fѤ}\YKK㽺hH0SӴLM`7^s®#+f89 \xҝƆ)fS jW:{C7iHyQpR}\N ǁŋ7M۷_'CS㠃FqzF0jjUIp]fl~zn(pEG>4t9<>}P&1 z;ivU[>Hbq'^ dҧ`0h6um8 ؁# :N]q[_>;C7oW j5`tPVFhv:alll(護:a~;|؂ g彘TK6iq$K=Cs ~ӘZ eM4 U(q Jo)4 M ^axSyzx*.$Y;ǃ] =<}H$bL=[o)PDˌE kzEd~:jtnUj5UUU:d,;x-&t@w@1cq^I. \tDnW It]@ fwF'O'M?nh'~+jԳHg< 6 8 !e0,H/ "Ob<<;;; YM=zűL&zKZߺu sV*Ob./8Gv/೒ Ic{0K}p^L8ّYNJ?4 ѹP\ӟezA,< BŞAx_ ٵdL;WLGG Qz|?zm7W2+($]֜dEX Q.՜6' |æ8*-GXeޛAVG}ɾ 2)&Ԍ<6/%dq]9aٳt5bz FT.,ybT{d@L&.;Cv{&#ER|FƔiG9KK\3P3zۻ7 Iv*m'8Y@C*GZ3C8ᤗՁt_Ν;7|0bkgt}իWgT7+t$3 #(JT*qܧRL KnQ G}LG$MmwK?(R/*;7[čfZ"iyڗ3̂k &7wqN#T(UctfYf줫kӿƵ. IDATyzzH$0RPR3H(%$G(ԀgOP%y=f>SuCC N5+JL ;8)qǸz*/|yb(J8991Q|,̞@T*T ⡊Dgyx$sD ]2&cdXf~& %qY3{ϰ1Y`F+[)7e52sT<6d,Ø[NwJTmST2Dqf6Իx̒L=ӣt߭3V /^UJta ^}UB!z {{{x>|#ucAr)P? v ԽTyH@H{bcNX}-}fv&[LD=z'UꬢN"$;!zHMk:+xm8񌧵-:TX~*emp5TJ\>OP(?~-bcc/^TNifıL*i7$d2lll_;kAHCG:zVM eE=Mk֊3'k<0(:scb-B\qIO HZ-YfvRocQ<Iy~x',9nD"%u]dzm2E~o3 ְ*'f49As- S]CkX]]Ykd2*r||axӶVJ<4rWd DBf8.03G'0f)e,ݯG2T,QZɱ. ʢAHd(=2%8h͊{] ^xf/~0]FgEљh5QiNd8==B-Jrt:ݷvX4sd lW:r'! \Br"W͊f5'ݬ/fُGE#r$+9]`f2y WuӴCtiT ^{5djqLBBAF=, o48<< ac۬"P(4sQ3dp=\VO/{g pL'r;g^G\9UGvZ:_Zr슕gr_x^/Q.gbFQ`;991\.g8z;jf}~8V5TffcL\@d1ҹ9y TpV=x~fkݮ*w:g`PuXm[kL;$=o@jn\dwwW^}U?n 1mNx"(Eƀ>`qqt3YN'OƊŞ23mX#̬U4 bζ8Xǭ3,↖Zf %N2{|6=d2i(>99ur4 }Z3jRe>Ç5&1rhp,1ϯJQ)7k iŰx2uAY0$ݹsDZ IݘYb K.@&uN q,--z@ϷV,B48a+=S9YLy̑x5~ufsAcڌ ɍ>'] sssWCEi_' YGD{zzѰ`Hj* ~PXZZB*k!F]9f)EqrrFP(t-]]y[[gZ-3K.p8:vp2D>q8qA gȱ>)3[e 'A~7,HN:[Κͦc,,3rW"h4l6ZONNP(> . NkHD*ʙb(;dpfP(!|^/*~ߢhdZayyY4;ցτv`$;d\tIr9r9˾8+ `n$M؝bƓZ2mg9 *B#l ϲ`9N ,53߆a4)[rH;,?{8v]&Lb8-Q.ŐL& Fg8 *HR6` r!bL60 pK'Aa6˗⼸oeTBTB(|[ZZB0D.CP@\F*B:{|fAcv''' B!dّ?sss۬\.;Ҋα܀gYLW+@477f'~YYU؇ fsLCer~XGT.U(AL(aǃJZ(L&H$Sܭd2p8lO`S:vl(mZ̥ 0ϑ?3z YP?[ ժt͟'ZZZ)7VVV% D iL^zGiJPrRid7 bXl``d28<o01L[j)^s\@4"KTT915ͱ AtVRy,RHGDJU䬯iCn2=|8)J1e04lQԤg=:??077bCa!dlQVDΰ tp lt:d2d2(( T\,; Y4o/eꫨjU nqqQu`a|>X^^Vl(iI v1UFfԪ1\,qrrRBBFȺJ郬(e:> ! : b ȿ`,Sfl F4937'epe!]1-batFsss8::R(9i~I'+/πP, `Gɤ쒅cVZ50YAzܜ(h]=/^D"P{#/@E쁜?*mL-..X,bkk ;;;^z O>Ulv=/D'݂/̇=)l}<W̐<ؙt:**,fO#]ՃiW\=sU+IYg t]VܰxU/qCrrngG9'Y1#>+0MOY7Dѫz'<+e)׶;RLjDB1SL ڍCQAlY4U̎ά|MWy^,,,0cK._ pݮA楗^ez~H# bRznUjֱ5_|VfյO~ y} )0dtNQ(T8ʴd %E@#3,A0LѠC+Zh1 B!EAfEP@0Uק2ƣnۚ6K9L&COOOG`Uy@JX`IH<4d{ ʹO$`P74 {d Yh4ptt4rbgSf/;>>6R\{'G5{Y:F2D(BPޞ-"jM"v-f^".^/Bu +f2ͩɚQ֛}VtzfU@sM"z`hI97ϳ^M( iKKKPTTl)bdyM $ɱ:L+F yJUkhiiI|>F1‰}t #,2,`fN?Œ?TՙbX^/jZߑ%L (JL~϶^Uh(1EYif߾"@IA .xƸΒw&1S & u]UNmDwdڂȮCg 8#Oxa;}6Sr$(:::BX4^N7wdκvT*0J&rڱ*<)r#IӜR=ydȒ9,//… j)74U,cr=jwȲt3n&;"k`hܬXα4Z r50Db^s՚tS'efō'''h6J{c͕.̧o~bIqrrb(Ik =F1??#Ub+5EtގS`ooL!VVVz 'ì_j4ťKpppr<}d.\Wx3{N RT Z=쒜dD׮]iŋ>裱C Vʤ> w8+IdPX$~xxxnxpd2xJR*`R&:"4K ƼxNOOb6~5=Eev!3D ;h`eDRYC< R)S#vq :_ϫ^N!Ak׮amm [[[xw~w7 ݙ^>ĝ;wɀfG@Khrl͠^&Jan܄n^;xs>? n}P ʩ'urN!v1U6ܻUVe>?7)T*J12XìDTliJ GbB^~e\rtPdtx|^j裏T=аkh`}]:677' 2ZLmS!LRgaH].ghʁUNqw q氃N'y]RiIX:v+c%FQEF"#Y"w5 cl69z;vUlllܹCX+ӫj`VJmllYf'H>/t6sXqRϧ9[:苔"sѐ'q[(&NOOQ,muY͗ .Ţliiil3K gv6Uj,_' {^qbRn߾"~_< 50+U[^[$u٪ժ1G\lGRT*X,ۘjf}hBfӌE,TmZxvXCV BAHOCY ^27neNӬްVppp::H;vUڌID,F-ֽy*>~wGE|N{nݺ5h4TG`C YdUS&Y"ZΣ)Vofhm]LdRiМ\vaFZM2?ZJZf lI[wqzzP?z=|s5EVSNpvդX"hrXs IDAT2nvvv M+R{onh`X,aJ#ԇ4,$nMA>rVMk1kj-]@4AMq?3.G:77yՁV(ΰEdmXTHH zt$@ |>oCDb*R*Hڭ[T;#{YYTtZGd ]|oDP(4p(AP(L,j"Jӎ/z<Ijvk+,#+AiRŗli&MIڙ~"kf) 9r ud4wV ݮrњd &F $3m|ONx<8t@%eMƭ[۷oc=gkooOa9ͻ*˸{Cyg sJTr.Cјj;HR* Xx2' Ţ DVAY"cx<,..N>8X`-rbabッ>CwL8k4תtd2h4jڕz֬|>B!u a}J\[`%[}[[[{}BN_of899ATBTz.f0vE"trvM}>B> uxKrc eᯓ,d-t蚃%~ X_GX ֬|>[ڧR ca(nݺ!FdqoRP((@TnP._ loocooX=M#5 ;?%  Fcl"3q6+=<*,2ӇRiNŹ9qJ%ND6Ls۝5,`[&A'Y1 bVh(kK$j,;ɆYX8݅^2J ߇LʠdW>G bڵk׮ڵky&ݻ-U&Q a4M겍y3zD 6FcYGELjLˮ'&a*x#a4I04(`EKezF8>>F0D*‚6=XM$ށLQ/]'myyޞ -,,eOO3_$0`2 FYQuC(*E!E?(Vε|>'^b}a.ꦛfx&`2D3 b~VKiܔ屴KEI]`ӧAA Z O]]lI3Ƭ2fCLG>77g[`CuBsf{LDda$Q'ddk6\1*r_!'oUmޜZQDZe̊Yr6~숔9;Y!Nh|ŐNQ(֎/{DZ};t2WH$pzz: P(ip:AȖXW@<}d# It 3)~^a=6' Ѩ}Rl:^ˎ- Hd]V,tЋ9,Q?8n2^[e\*2+',>3hZ<h4Tb[;˗ BIUÇxWgs]VN?8~t}/y^R)n˜crX53r|Gn_~_TⓟOg'6cMeL)h3 !EǑP*h4* Yd+UMRfdYq#rW-WM:.ݖ,k63[!MVߛ"!EK0H$CT:N)Z-T*3P24>RKg?q]NR]+?/_6(SӲ٬z|jO<,{q|{g}?~WU|k_C>W#M8d2$0A8ϫBe }~f)$Y m'ŧ3/vf:+ҏF2c:鴚d=>.8 x  beeEG2 @ {Y+n$,"H۞%#&J@`Տ.xOJpl>.t:yvqfrBN!,?OK$R*FSyVuS$.AAF?PckkK=zU5]X\\D @/KDQFh49PqmmM򗿌ǏsPȸloowy(R;T`*yORy \T !yޥ,.ˆ =&AUypp Y7NmsRvAD2uFN*eۿz|XZZRuMNvNY @"{aR S+ *tT=-f*?S*KРylRHR+YDJ~؎O*mXEGju 1?ZL|^/+pBϵMQQޗfҨm8lZg@Q^>|?o}[v.^xzpp`x[|>,,,n#`sswp}U_yVҕ+WN 5FQ-V&ӽ%X74,>^Fց@A9=E6nV+vޅFn#;:T* e>l_. Q>7u;P 'ny^auIH!A445^%U%xW.c$ə)1 {jɽXZcڌkANڞa>O`!Tfi3;NQ$`0x9cdm:YTZ/KG?… KAKKK= Z>!Jamm ߿C?|^{ oToݺ҅v)vywBEf Y[c53UfWoHgРkzE%Jyc86VρN,'DwqoR):9bP W T**+@Ā@wt2tɲ'\d7-tT'~ Ѓ&Na{|X^^6Y@fHjHNgn je nQPӧO =fsP@1VL% D"KPv#/=:J.2Ї ta`vя~*_|X__W:f"+#f8F4U]WU\t *=G>ק$"HqF(+YVsXK );bhؓvd`c.Xע3#owG P@{UOި~@oxd a>(/,,eAg һF0襙! (vǩNъ@ZL4<0u:-I9SyR8E'^>pM$nkpldG+k$b=ӓdz}^U#RZ@G~ +%D6AYCUH 56M|O֩z Y(B$136 (^uߙr:krHP$?'ADu:kZ^C 7M2lM>?9ϑ#tu-MZg0>}ۿU/ZmX 2NZR('bnnND3sq֭MlooVVV4{f UO[V>dt{0-:b d2ZH+& Sm@D%|RczjZgxᡈki"}-SgB`hx#͢nXCG...6<<2(hK8Yw0hqU,[G@Z"!T-1T XXXPJ6BX=i*I@/~Ԥ777o7n‚8  x>Ђ/ACLFV_ug |_l,׿u<|p3ׯcoor劺7g-e|֌|^W}o^GVԏ(3V:kuP1J'CN66s^xQ5췓ZDf_ꇑ:LHY#1-L"JV Hg!H1;rG$1;aZfF##d!(S+L8СA5̔ys]UGQ5d:Ef”Ӆl 嵳{jM ; (;Ȝ0:^saO;}$ 5apfPG}ؖUassW^U{'q:WNcb0X8V~|^Z VVV 9)hkk W\ښD;;;xwo|7np|ܻwO O UUZQsIZfeX ҋAP ,b)zs^N3G*9Sb7ߑd( 0iYrРCٹG5n$Ո , rvlH!'1"SsCLEtd^Fl `^V)*RjTF6?#Y00nEJyH0#AL VMI$,Sd,ȶzV޹kxz7)zq,.ݻw婢կ~kkk O}>S6Ӌ2l:Cz@~?2ְwÇ};eNc=QNL2,T>99qS{A:P;bv :S{rrrPuIPH~2Dayy~)6KEɤ;77߯/@ e4E6.횦"b$eQ X2rZ)Kd;oyҒE"[IFH@ 0֖jj !)wݠa0~V@~be|>6ѨbU64ckZ{CQ/\.蠈7HZzj@SgJE n`Htfo?֖uEdK.n[䚐`zrZj_o >D),\ ä)J {X/f}} zY`}||GB!%aբ("%?k e. }6;l<{OVfAT“'O@ yC,b6cFލٳUKϕJ Tgl9I,kj~rri;D0Hf1[C]eÀo׫ V %Xi4CL$>hưuNZ \DyNz=v:z??3|ō7)R (b:loo+" IDATr3QlL16~?]UCsxxh4Mlnn*H{֭[QuUmloop8<0pJ=/ /dX%֭j`g=&>Ha=\ܓtlf~%* kX,4ӆ~KKKb Z܇C߸02!džwqc;b(73&Id2C}E#}9NIUb>W}p#ۗ*kql)Eg.YY8![yZU愳s=%:Eag[*ElGLݲ;u֭1??U7 _02Z!uo|>XE\oz]}&5ӫZg:.^MTqccC=zwbkk X__W{]3&j ve9d06ٌ_?Ɩ$h;ZٛR@dr U^1DỰ[^{`-^`xXdfqxxUƒH$dTJsƠvo` VNN!1"f:KZezHb72-+0nHY$YLxpUt{zzC/v's2\iZgT"gl0w+DbK˛8"|V^OF /cuuJOH@L2UV)Pa\rQSψٲHO1dJl`UʭZX^^ݻwq}U'e?ģGo} W\A$Q7YP-g!Å lJnXLfG)kYUa*Pv\.Rgei@P.Muؒ@ ^zg̀guǦ ֬aO?"P%sssrT$P(l˓L ٩gSg' 8`/JTDİZJc3) 2|vdukE`;H*١@ R2t2EvLX%; w݁cFQؽR"dzL r9'?O>k7|׮]38t|DZXL~]R|o\J?6"=G",,,ɓ'i4 5V:W"y\_o*n{^Sόj4깘= IM G+\{fv< y`9,PyB!e,,,ƖB$#G7tuMz\.gz vA14+(߯ ەh4db׃ .]F-Q=QB D: [c2QO<1Cloo#+N @*"ɨ˂WH$bhÏbX[[b曪=Ν;x,=k9C~vZgb㺮gs9ùq5#T(Vj(/.ЇB/Biy) "HZMaM!EaرErPu69sΙ3$-{-r.Zߺr ]vxjvj3Pd^̍be"Qqzg`Э= eY:s qjt=:888^V鴊:NHC]b>Ca3ZX X,ҽ{ ={8HNΫ3Dv24f{*儢4b`xd ӎy%Sנ({ FݻIGyTС1L 6%2v{ ٵSSS  GFVds>'BTq6Mt"tY"qx9dZo!)W!1Tt I²\U۶#>hԷG |]GYD`U *_jZ(<-evI<'(+W֢tFmR%}{#d7nܰ[V=J@~?4ȱxwhwwHׄC@*BbPvvQ݁?T'`:jCgi,<:PuNC5ȇV*mrk4X,r8Q{gŨVj!-ڌDѸ2'5ՍҸFHC]UF!2(Tlx< Ž&;&;K jc=p)soBGx sPi.v АU/ݪGypש5P($Vigg677i}}] $>߽{r-..} G"ä{buuU d5l /ɫs/ ͦNQsP(!QkL2/s[lZz?)E8v`rSu{sMTm ^p,Ym塾&pOm}=fΝ;G`J ;eբP(d?! '­0EFhLeԌ _1:F@!F'=Q@4)q(4VHee|>W<@PAކ )HI5VM"LvE轒%Rp4{\ /Mj7n0aD(HzC;mSߍ# ޒv2d(իW]:9x a𰲲Bd2i|J=qX39fyDT׹'{3'(uB77Jǒ3`$QSr$ ,4M#il#VC.YnP XlL5ڨ )(T伣d0j g<u-Ua̬#Xv}D ~ԐdfI?O*ܰɅBJgy.^ݾ$vꁀ 6IeU{HlRZey2Q8Wru|HߺukdYO?M/.?ڵk|YcĺznrQgĔ%I[}QLRlä pLgj_W>HG!d %9d6-..nY~ot!|y,ISal@XiaTklReuPQF0 Rd =̮M~- zh2^gpNOX7Sv}$Gm"[ttLMM:b@@aO&7oP$ƸH$¡0nŽ bEb5*J ~?'WfdY>C"t7E_r9H$_Gu. ժ}#IHbq3eޛQSoHD1by\yjjۇj5CP4*WbpX*M/=T%r*x"HVi#GYUE;SQ`u%Bi;yLCXy6€NCgk "w6ZR;U9ƏH=@w*FiNӡd2KUF^_;j>p5bH˶pbx DկRѠ=>Gd'Y^GVkajZ-%3t8 D{{9>3*׿nܸ?B8 ̍ eu&BrB޵ZDDE֟ۆYbI:#C^  [ (QkF58Kf Z3DA`Նt:jt86@q phU0b1]+ ; ݄p8A*{o +(gBDNxR?CffG~΅#Att.?#"zXEe\moo_NW\VEӵ-P%b0=o诨`(*UN4---fN*Z___N/3f7nЅ 92DBVNK3uz L-pw-]ëXVpd1*Kc(P4*{hvv *;zQ},Fa-pDe>t-(#} $-.5lfFuBL߈M2<^nw0*M܇9UedFÏ>V`+ I5vs}jǫuc˾1 2wvݸqfVTv:^!,WEQ]t^g0NiaaAll7>Ar fre[[[wu_|REZ#%*"mNc"yabvYuq{g2f^BCvhD3d#{?NE.SuL&E"I1~fIv߿OΝ(8caa'dVVXq-DN2(Ay\a8hTU*(juygVjՙ`#:zNP-&DsP̵FN֐Ehϊ GP:4&Zs*a|;'|r봹܇K!DjNcA4{ԁ>eC\[EZ]]W^yvww]͈&[\nnn!r:$#6,\jPrb:J3)Xdqh%'Tp \.3sE|nv_&VC LzR9@Ν;Gׁif QVլe, 2=Oj fCd `$>"$3Qf> U>z]g<$W;$0Ҡ#(@Ȩ. QݶJS U]y=No޼Mv֌磝~ȃ0n Xj< ={5FYd9HWWW>ݾ}T*=#5\"8Nnj\i\>i(#82j8e~"^c@ ٬Tߧ|>v\cbHŋn5"l\xP{ezCҡ2njFmn>+P:VTXʰgIQ;`x%T ISU8vUYRod8YS n:C''^rV&WKaR)CvHDy3 J%ne0ztmZYYW2`ByI4wR{(J@R#a,]{jcm\B jRU2g}%Vժ Djv=1aS8xΞ=Kl0'@ a4B>iK?9):cc yyFyQуw1AizRVLMMHWct#P2E9vpH333ܗL&)LrqkE&CkGrxB^q>Ϩ؈R .Ȱm䅩bv>7t2bFh}}Q,eV\@ t0RPZHӞvM>5 鐕Et.zffFgVptZ햪KNf{{+ V ,(y-]ǹGVs>t:\z@tl:jPmTa;j !/F y@j5]6;Q2yb!J^&KqEghh˶,fS"k A՟R/ lQ3c[7 t]I&8eub b1?U*_eY:̐>WVV֭[ b`rR).:sSh0NԬ : :2A{sjL1vKrM9Nt=NjLl7Z&! HZjZ-CHf=.(a/k(C0Df-R 8ޣ݃C2jzjUꘆv IDATRHY&e,~Sn\.>Z-]^ʲB]q{r]|sZÊ5z=%0B Ca˗/sW`Ft:MlAK/ . J$ `nyMMvUuY˸'Yr4ZW0$? 9Lt$ ?Nj`l|O*n\H f9Kh4XLOEy=P\ 7Evw`:zfhe2$ؔlGVB8w]'|TThggVVVhyyl> gff(sw5)@)=mh0PVNM>/h-4bFkXv$3KfW!_v/4$<_i[:FpۣRD`ɤPʩz@bg@BVV 3 (k3̱'Q "Mz˖%n@[ӱ z4f 7>gR*({\P u}M!r* J<Ν*&ɬ3굪i6ŋܬ\|fz/tl~_ I!!MXYYl;;; iii^JW\!MӸS4ۃoMΝD"S3Z6bM|.b1cedz*аǭ=^h\%AXIx,49J&I4isssFiooΙ3g ?PN M UUSAByGZxp4bn 1Ef`H`چ!/RAk7xb1f6jV8 =3@=<_'F^dB! Zx6*Ց`666ēO>y=^rnw%"q:VWW%zޣu}6mmmښ5F3aer$wGW$l'YITJ%yUHUN0FRiHqrL #A7eQ[M6g,4NYeIT }T)qR#0.Z;ZZlT)tBvUAJeCVUhff1^ҳH* ?9q!& CqAiLɆci_$xqFIx=C /R8H$(sR=jtf!իlNjo$ҥKt%zg?13Ewޥwr8myy\BtQ$ 5d2\ey19pucZD2>U^3 kH&]cˆM1c;鐩s3^iG42q&VI(9tP(J041@zradFq`0h3աd`edԅ(evJPTT8YC`k;QT넧2n6 QFG8]ޗYBɎg è]HA2G`FiP]YDh0pR/ q!K}]Me{͞X.T^I{X 2---mllO?MO?4mllF ۷9Nwޡ^{(ѳ>KN4Mu`uOZʹZ{VFa NƱF!3ďI4YmC,uGl9W1E&OMMi#|>uf4JrR)^7Xseem,㔃hae+zB!4 M_mĩF6)QGբxNo#Le \n`dp8l)Ny1'#Åب~O͏ujܧlHLMtpneo1u(SۥCS UӝFftVzp*jTI#r*l0'UMbTK&B3|2 H$2IZ<H GTvaݸ~f& jQd[%k a:@.*F9lll0ca={ア3w:Pd52ZMW.͜)4KKW;ybݓL${mXd{U?R,Qa Ǭ?YH$tsg˞vI9I:[.̗>GJK ߭"Y">PaX,Rp8qH&1-ˬkT/u|VՊ,z5% ^G^;/4|ilչaM(,z FX&6˼#7 nGoG.|n.>C3ap9jT*t! &%Q8>g RHFy*5/sn)0c*'<@$ËH3Xj:@i,3jqDeRlGCm8СXJ(Fy IpҌI.WT*:JUꫛ́.`3'/fF.Ze%B⠝Y&XJG^+ԄjsnπP!<[u<.[Xl22P(0qk62>?(eku>c4y~0{.=30 i \6==!-G"h4H4t: $Z x(b { 6R0hE:${Ԙku@?MV!gHd2Pa7L`0U!FA`"}V>ҀH&9fe9 /H/BHf!+& @QOx2&/uP8;)0$խ@T !'C[~7X^w bP(ShT{VyHgУ6x|wnfܱ,v~Ν;t-zgK_=666__fիg'0[YY wmC$S`dO.Ө}033LxfEh^j'$o q1|rp8AZT]2H$XPQ^k01$2AK4Y݇S/DU#PFϳޞ|u]*Gr2'azNsssAj\^a/I{"P(kF+++tuC{bccԱ[__L&cL "b49\!2f$`D" d X֢[YA` u;Y Us̨ɳl=GzYjo.ڔv7ؖH$²3R|QVMMM8-P6VVnmL?B d2E0$UMu]#@&s>J@zfY0U^B C`,I30A%Uiw 0DJXG`Qd v;1\Yy.׏]fwܡ-Z]]կR6ՉBCJZϒ5YunY<bXC $Cv%^"91b{ 815OH2xҎ, (ΐcHTVmY{!#J3sVBnH*[͑dvhl@i[ϫԢ y1gΐZ]#*^mKJ&GԶ;t慖j"ȊzTJ#(ޟN9 21U~)-$-F@oX1`yAÞ֬c`ȔR=)8 藿%NO֭[KByoob8KUT:xx ݸq"Z-S/#sbw&󤦏ȢBIͭņS6M(0d 7Q!$w:#K_u ȋ&F.lˤ֣Ʀ4O^+30"# :,DɤNl6u .;C(=Gkp9y7*!Czn CVܫ.Ì.!#֑pfIJh_sC4p~C#t.>я~g>WF>N:Kh{{[`Seqx/EQ* \]w\C2e^(eKZe/ѧL3v4mjp`;+/3GE7W3 B4;;bbtpu#F&HGAz*T5p C!Σ.!ˆFȭ|:i$/b֬h2Ypב^jV!#LZѸv0kfl =ijAxUB6zIcrr3RKHR |j*$Z$36|l![SSST(͛7]@ p}\DqݜOMM 3s ~?D0iuvwߥB@ϟ,/bHl!lf%hF"یh0J;fZdжs9NӔH$;w:7j_ce׍x^U8Rm"G a3={ГxT*PQQ؃z^$SeCGQ}(P!. i]K\RFxUusԄ|BCeCsWlOv6DNj,%I4MPJvȬ}۱B;;;h40{EݳJ:Gc=F;;;t=x7|&\Γ5Ja3Tgڵx P&&֞? J%L8(*0ݐ|>Bhnn(zLRVXr0Y@v (ÔanR3łu@ Ra hYm^ЎиQuЉj#!F#i­ üzfve>anwNlrzz}B>*Mi;_CeZBST)Aa}\|'QA"YP(trhFKKKf0ijA MQo+WI\FtI>(seƜ^{`j @Ďd RYD]A>LV:kbȌV*ɴ ܽ!5J[K ٕ`F 4 NkAFH*UGQWmgggc3a١mhעV Ar[HMyedR ^%(XduS1.[sE"[>TI>O#wDFnnɺH ]*Q;գ,RGDF9ayr@iѠ]JR3Zig' N" mllЕ+W8H$'F8';4J`0pP(Ay@#ziiS@del܄dUa_+v,HFԶA?QR :|VK*3]o([)Y1yOp81б"5#H6i7o\LMMEzsL Pb&SS^uڔ2+ z"Z*=,lq~‚ȀaHS5Iu[Rp]>,ԁnK@fggizz2PXh}}nݺE);$A2+DCu^M6H#ڊ#>`J%* Dt[$A%f6̆6s6KZ8ȮCNK$:;"a26M 4NCLf䙀k[6< p-YA2+fl#ĝYa$+)vV?6SHD"a ~jnD>pڦCp^pP#!(x\H$8?OVw:moo~M,?^'/"?Jfl>F=xW:)RS|>+IYNCBq(f:-,,0K$#U8!vf'PNF$WPnAB*[&'!qM@Y{1JBQS^R҈BӮvk P(xYZ-?55eiy$pidHnv#q҈ːdZ֑Dݵŋl5Ml6{?E:R_~٢UIwӡ]e(|&`uMAX[XXk~B*#5;$A `0=ʜR a.Bp{{{|U2dtJWR:}|ș3g8. @N#yjId2tEWˍIUQeN 2GxFe+ofH9iQ@B|NnF9y~펺響)ݼKh#mBB)ƒɤN x@RNCk;LZoAJ$JrpbY1G333/{1LkkkŌJ4-..R4:PDFȫ7J~h~~Dm盘tKUvCԯRUA|R8HOː{e5JXPti#3PlqOV3@"n{}yG</rT`! 'KMا_ IkIP(ATß҈5J- ȵ@Ty*FH 8:@[U@cdn\v" XGHz)NڢwݻwY+++߁J,")qC*Jb1[`0{q8[ ;!ڧR)cm_VD>OVPUiƺ] *v❱Xl,'`[P:%IfvwwܹsF~Td2ylFCﳳ*#ګ( LӡJrM ' C)V^w낁<z d-PDKxvN˫aj/3@d:9q5tg.ej4Jl1nDjqqyzh}}"bcss:666e5D&it:K\=l17h.;NYX#h|ک6v0{{{XEx%O"HEp T|>yZv>89co"D2MI !Y*.sݢh}ialo;CEH VUnCLCQ wkI#k˂Тt¦uYB,lj$vYO}SDhuuRtX!p>L,Uv*+uL['Nse[}z V>-,,&bKDH\ņ¿[(0,^kq4 ½;xT*})$q7MfT`G$C9@wâCplԨJ%ZFTb8R* 4EDQ6}.ѯڕyy-,,0uR`wHaC8F~/[~CҎ:a?thfggX,r VP(žT_?n# C ݳEkZG-4C*ce#U ^i gjQ D"Snd6VA w$<\$2|4ȴ)%XA<Ϟ=K\eַEtg,T*5 '?L&Afloo[վ FsM(wuߥR)v.PJ}\5033$QP(nl69hl.+|C{H뻻:lJ$vY:c^Ols ZҢMs:'CiUP^TC,h ft:T*>6Ǚ;td1*Uh?͚]u *T$ 2Ҡ b5T$RC6/j %xBW>PJ#8)?oxfvvv݂ԄC # Bz ~mfn޼y" vӂ6fEsɃ">NCWJEv2ƽ T |l>rƵVx3pKP$+l|7r*e !r';$Cc~_>rxPƢ1d xlgda>؏;da:|lQM D PCeN*}>'h4ǐmK# 1}cTY\\|@ T*w sWmf z饗MMPi0XӃׂ 'KCͻ+$aὒk7¥vGw]#qr`HI'`k+s{XD" Z { ϪjQUgu fe3]O#a'CU# Ej.)(Z.0cPVbW:Uف 2g$ j_T(JY&C1w9v-4AXhP mw CȓFf` ‹Qlp9s8/0^2?Qh8i}hssS׾d^ƍtҥcfkT"/~Th4MOOdl.Gޑ!IgmWkI:Hf ,@ژ 50 woo+M?DhQ2$Q/EȣcyB[mڳMޓNOkNHlӴ3oF]#=CLp h`pv3ֺQWlg) r?29vCf :Dk*+6Ooo:}%^0Y]]H2/L!8,,,D3e.Y,㪯Q,yb);qjcݮ.1ZI;?θki0pXZìl @< vT Qb#/~NӐ!>TZ`ˆj;'#;0Vnqȳ I1=' 43PDF"f:S6<1܆C@2A2?.Tl;\-#:77(3}7*A/"} 433CB… ݆ZݻwڵkySm az:wuMQjm.pHjvMr3:NOOn8 0jh=PuE$YYqZ! IݦVE~4M3=L:<<$(wkSp#'T&:AE_$;]./Cr̀N6LB`0`Fy74s}p8LH|}ZYYT*SMMM̌au ަnKQ.w}H=j*8gffN ߧ@ @LCY;^ס/ ,)+.}J$AM\JBPj=55:]333c@x k CO)plߧ}t:Xn?$"yܪD"G첓 |Y~Nh`Asb86:8`[ws JPߧGz@ FCFDQ`g4"Z-f[T|~%x)dKJE"G|~03HGR>*CNQ>H$In5 *9B+B xd|TTj*2@H݃bRCV%{ܹs_>z7޽{tynS"w~?;T;{{{z'[oEo=py(PScƮR$X,F~< eg,8f4MӁg\4Mc֧X,RöR0v郃;l'Qءg:b9!J%j6\@fIH>|V9@+BXK IDAT9J$L&@78u~!H9s>88๵bӇ|%DAR'響)G'v /+7"r4M|)˦9I`H9Ѡ$%,^}yj5fas*mPМBhH /d$e+3(t:**YWP* ~cQ{K&t655ō$ #B.B2B2EnA?LKKKoƫJ魷ޢ{^~ez=ZHt:M ݣ:.:[tZYYx 'b$ZG-!U*ā#Pe8vy3jP(0;\}:s挧X,FjS2 afP$#;S.( 6+ qd=(N5 Ba1qSOFL ɏsHæ!txDndYm\Tӕ 'sŐ.,pr0#"P<Ӂdlhт1B,B\=3 N܄-吡~c3+?iuu/ҧ?iZ^^jJJ* ݽ{:eZYY}8Dxr@.\@z-١.A`#pqp7 :<1Dx*n%e T}"'@`s`V055Eb='It}'Go&/| l(1Iy'I:qb"BDŽpOn30Aŝ{sS?lN3,q2)q g+TKjvmجdHCm`7Y eP{#w 0d] Üm^ +Q̉!J,7ZI TTG@d¾LRWH$k֭[77ӟ:-,,8*+gas{|`2-DD3(jZBDUxt8ٳg'V?==@ @PT.j54Br>pJGXw-;D2 NTF !O =@kߧ]~v`"2gc kO䤪QJ>UHCbx^xpƱU(Ka32Rsk[Й FQ x`c%h zfs^P~D 5M`_T $ub.:$(zEN'P٤o|Azi{{677innV%sЌW_}ɤ! лBdU =㴵5R|G 9|jFysIvr[_#dZ-ۣP(xݹS87PF@ sIa0Cnrwn6ijsr8zNU"P,sΫCx5h4ZRi ށNJqmfmMv226==h˰6@ 5&}=!9P  !'̕Y(IY  Cl6Ko^X8il݁vQ:f@Y+`]mĊJ 7 "bACM&Lm4b?n򉰗$P(0uF-G^&OOOsH֩X%l,(D>.Rgmk"e-' .K0+nq:$ka3x'0];. BB:FV^.DQX׹% ;vU3ݰCP?>@ODruO?O(FHSX=~ް֩)ݥ "OӖe`|>!ݹsAO<ምk*NO? !'qr^u,03,@ }P4wH& CJHK5; CRVHw"V)sG%: (cwÅwϲHH6,#$""d#Ս֌%yBCt^w Xmuo7NsebHVp=F T*51#lv 14:q&"O0Pt;.\͛7봱Aoߦ-""ڢ-Z[[#"%{eYZXX}_<Z4#O /s= [{94/}KKkkkk2]tɓ FR5{[*;=1ɤ+vMPsQC4JR(7T*يD``0(Smn{ȲsK$JGyZ dlH$€Z.j$EHb5PupdsqfqMCȵBH聍@dWF\0_X;"F=avXwE""aPiSO=EB666hss_p$ݻwW_-'~ߦ|;N=쳎C8z=D<t#~ , tg`IQ>sz 6yL&CT՘9;;KbŢm}= ^. 8`p0O$ypI C5]aX^ȣ 0 G@*܉Gf KL6^ٽ^Dj|hbMJ )]p)9K8tۿM?8;S&!M8O䩧 󴻻K:0 ?&KN<\_Fkkkf駟v}ߦuTg\.fPYRk&Lr [שV;NaڅKP$:KDɐA3r&97xbj5*J\lgH <hqTdH@bREZH$ENe' JӔNˤi .P#5///:ϛ7n&DL\j]rn޼Ikkk/z: ȩls y= aBpzVuJ"hLzn8lYTlQ[%JRDq?VIVm&;U2$ԙ%;[)@%x~7x۷;86s!@ha L8<[dߋ.UݬW5%-îaM9 fox͚wsi5M;22iE"J`j._|{eW_o~饗^t:*ƍ:{DzKp&3wA+vH$3!j|蟖6*(#l'iy[`ymM  ^+2[*&~^%m\T4 !'eH-"j$H' JӴ@L$$6ڝ$2ɜ⻜"r IR+u Cr9|gA' "]Kh\~5=7<KFQCSDH"&%E6,C:(dME! RЬ*:"+y.>/;9sn#8"gΜ]yyCcA+TfGE9wR)c-L86(ɤ҈JOlL\1rBXĆyxx(3@L߿?g1s=cNCh׉qX޳͛Z޵U $[Ɋ iW!E0?Dsd̓=HL8wꩧd2)o̟i)gXwfe|X, K )f6/7!Ng~Q ya\ KT4]x.^HD’ZbwJrbuQD"ckf97u~91CC%!- 2g9/q@v+02M B~+d@^faXqVEO= Em+ hee~kݾ}}:Q{5Mʒ(hk;Sl)O4‚Z\}:D`0(/V>HB"3 ]O`盘U!O8jN0V- Г؎Jnhn MD_xry= BJ!TEnp( 3Z]Vx9LHv̸E w]Z[[#"[nQѰ}[tE!CY}<3c IxvMpG"pa/ =S8@DPN3F;*wcݘBIS GBpdLpTh9T"8Z0%g[%S8 VDf'l ԰qw\&P)\!op5( si@"lwENTg}TR(ǘ׉q>_W7|2 ݹsV>Z,Y ( b>RDjU(o`נa?θ.@iood*/ Wg3s!"a iDXӭ&]6O !B; as3 F(JI@>*|JkJ7w;ϻQ8Q@8lz}b [Թ#֎J7>lw&('@P|a 2tf/+vD+3z6777G%* A<@A%zB/igg677-dx Z^^VߝUcYZ1|^H aFLK6[ u,HЃ BX jѥK(n:!j4,89@iMR̔YTsI.Bf~AA+n=_nuĮG# 4̈́$' 3̵ش)'f)Ueg $dq!'r&hX<=ZZZ:AfPoGb`aeݦP(DkkkL\>CzWl6z<W"J777k_{jսn CGyƹ +W-UV >SEE47| @;,aXYγ%Bh3jeH.FT<18!=CdԐJ?!lfecZTd.)B *ѩb"5" y-.' J$BQJ0|Z-i6B2fW=$z MD5@fD3wNU"P;*E6-..Myp}hmmrh`wfg~ls "NiffFfT*&al҈yPHFuys+ Dg`UUK{L'%1#Ȟ8Z? <.]47kIU"yH Zxff…߃W`9eh;Lz),(c ld܀qCex@5+n ͠!.AR='NoFjf0Ud2޽{ N`m`qR477'ǁ amH$5üDhii4EQ{^A,gر S$=x@!"Q,+<ԍjб"Dv! SD7z|+ҫ ew9kɏBWaY]`shS_رATUh4*\^2׬r)~_q:݅BvvvJtʕ /KZʋm! Cqߏۂ| N]>D%9)VI d9A| y_F%V}$6sUMOF+L#jDNLA7'CNɅ4n2ٿWnwNV؁&}r(;XYY za~Mp8,r#ݽ{677M{Z[[l6KlJmhg`uFiyj*rfd?!;H=;;kN G[Q~aP=.x'{x2KppE9Û\faB.NJ5y*NVԨ`.@~?0u:݅{`9i3y YRv%ɉ"!~op8WVLȸN`Xhx99n\Ub?29AoFk ,HcnJ&"vr9z! & SdݻBUB&Hrr5TPl6+zOSabtz/ Rj;xd2 v/PvVQ8t||,+]pI*NZݨex|Ѩd\43ռ<#pF8 lv J[nb+^JWd>"r. '0 ݰS'T]Qn<\2v#uQ0E\i-jkGnԼ[ {2r)5;ޣѨ0B5Lu4Cfz*hP(LD`#6_7oYY6׵@^E7 ƽ^o`ӪPRܜm{1ggg˟ft1+Ÿ  a](Z$ 7BGfo;2!r Za3^ʛj$8ѫ4==-N89$Ff(0S(وs¤W"DP$/'C/O3Ch@T܍\5xq,6f&a~Bk|&izWʕ+'6\T . ''JZ!0 (r6YWkQ~fj8s)s׼!xsӜ/ GNq իWłk'I""կ~E~_X6;;+®F.fypBm*򘖗)DSSSty[Ȍ\P맧 ɤ*c$6@ n;I1@ٴu'QjL@9e52Řt钵54P8} mY3S4ٹ-p x2BΏc8>Q oUCAN 6P(4b$ Ih(nņ]fTyCFiC%WhuuUabKKKfEbʉ1D^و!OMfњ}:=|P4FF'!0}VPlf^('wTq87q3jJ% b(0#*02wB[>t]XmO7I )6XlGox<>`,i̍ja:Qx"O3|2p†Hqa@vސ`&Ve^Ͳ,(@aW(i7ՙz ,^SPv-rxӰwtC6xgTn8 08C;fӑq>2z'UP,'7rcYOE!Bx'q! 4 VNy/zաo;$d&9J%* #]/̥y,!'dg B&;a.T*όR$wxE/vIi<oݺ5t '|ϟd2IDB#VE /ScmC: ոVE CSSSbz=pʜ\Ag Cۤ GxO jHȐ-BdwFS'qРa"w98`R0_3;::|>ORE3W̴њ`(?<<|>?p<<<D|* U*P yL\B0qA,ݽ{0DQ eaj5w c04€9jws9|\D"teD"l6@x?UUj ?\\sx0N8Y>NO"8=J%:88‚cke.Pz [ˢR4>Oȥfe;W~prLi"5<*ޔ&r\.zuXLa>[nX@xi1PϻӃMBI7&k<\.7lTt:C" (H~g n׮]3F!i3"`Wnd%' 9>v:1g&^ 2N hR>ё7|v2QPeՕIVxXDN b'ȐDjyO$H$ij@'?Dh#2.H,q#T*ѣGDD|S O>}˹Fh(J $rmo gssw#2RBѠ ܕNMpϵ?pNB Ժ.|>JR477'>nlrrغRs0k? BOBs"BD$2Z\\bh' H ^qD_Az=j4T.O i 'zRL> HO^g$lnn^|,J%WR)Z`08l#dH$:<<YbWJ?)BL.(eڵkL&… ý{D+bJj6@C-SEw Â!QV6s@2=5!G GbQDmEW gNU"~IgѲ4"T*, UHOV+mFr CPzC&H F GZzE(zIE"!}# H~P4 roj#==exwޡifeya"ϢK.YλVvOȽ4==k`&|k6Ri"xrXu O"v, CܷQ!=v '>Lrr5T"S eV`%B=U1*,= 62CNK9B ' 'LKrS11B\>JPJ|r ?fUCUsnЏ~#z瞳!1& 2DFxAVb\]'~@rj`p 'NRDB x*;)2X#DH:wTE8I$Ccnp_V: ZZGJ< sx;\{#x:͈'-\8)ֺa\kѷm١ t ̶C?!%I˭;Ddi2u0E|C7x$ZC%*ˎIA09!"kTN շO21rrh"9O5ku`u:G @6XM`F$$2-Z2BȭT!T2ɟ݊5 $Fn9laNC0Y*\.*w荇g}qRLGqCUcdl?O/~AV20wޥ\.gݾ})L yBqeZ>Hz;wNgXfibk۴8kbN<ժh '[iBp/CYTz p8l$155Ex\4^bdFŦ'9c"CHPj @`}9٘oL0A#%EAj 7 #ȉ$,ȋdhgghz2^Gt5~C2` dۺJhssܹCm$TдЎKPPɫSSSL&J_0ղ 9 pBG""O?MFC'%)5)D8![HV/xX0kzdĨL^.@) ,vCTnV{TU#DX"EV~ERITp?#+ Gr%ƃDG7y V&W{Yz뭷mooG}$Hښ nUV}g!a00-@.~K/t"EZab$ ^}tցޞPMwX W,;1i6<~??:RS*z"R$qT3Aͻuoxm=Aay_Pyb |MRe'~{ؠ\.G\^/=쳶>,<.QT%h>k߸qnܸa"433Cb: @鍼B>~n*4r_EdZ5Z-*TNEQ +JY0jD9>-ѳ ab *x&."{3G\i›y&“ޕ ~h6”ƍs;C /s=gnWTm򹈊? 8R]C|}xF!Hȉ[i bRvf<;*C~?EQT*"4D"!?9%ϨX,HA $Oz=ˌẊy]ؾ):>>aӌ';QLxBgNJȔh4*L;& &I\Y'ܛb8bT*Er+Uw~@Et@r+Vb_}Mc;I"C 7 ̡)_~w)Q&fTA mmm2떟 F"A<너f1P2r4J~*`;e'= &D %xZ0`9 dOx8#Zݻw~顭n&Cݹs>_|VVVt⇷C[8%CPx_I l͍u]2YX. _9#JzmJ)uh W="Ӝ">|ݔTAAa2Nm56 5F[oELiiiVWWiaaA*qR?o}_O?`D c"e#ohp~d{{{U~p ^?GWӰFW*w-dYa&O:l3ځL}E|8Gݾ}eL&CDshiirhPl6K[[[_gyFs2q!+,4 ѹ~TՓq@0T&ԉZ"/0PqƛטwC7)f\⹓MP(hZԊ *((B ov=ѸO@Z4"Dz,V(((B ^ V* )((((((( image/svg+xml lager-0.1.1/doc/_static/logo-front.svg000066400000000000000000000226661451275761400176350ustar00rootroot00000000000000 image/svg+xml lager-0.1.1/doc/_static/logo.svg000066400000000000000000000231311451275761400164730ustar00rootroot00000000000000 image/svg+xml lager-0.1.1/doc/_static/mit.png000066400000000000000000000277651451275761400163320ustar00rootroot00000000000000PNG  IHDRdCH!zTXtRaw profile type exifxڭiv8sq77g`hMeVNwKUR( _`=eSl-6yQOgyj~s_79x >ys?_鼟>]ׁ@] ^(ua_ u3-[-0_k7C9eW"?m,minmNg-t~g~zw}kx?ܼ /.}isvJ]x|vۧ;z忾ow_5Ϊgv=f_z}yCֺ|On|W\9E k[=do7 oVvBѓpm۽t/W 㸘#ѷQB9'[z+<眱;/k_k隹2ε}W$+uL!U#Brd]0dAD~_ 2! 7)qD لoHMc"~JPO!ŔRN%R!+r.YK(K)M 5Ts-V{-z禝+w>9GqGuѧ738̳:˯WYuշۄҎ;ˮ~N͓3$,'Oǽs3r_xWo CJC߽F{kTZ s9m9Y lIO [>-goKQ8u+=3N"Ii~8X0f:Ʒtra g>pd6p"_z 5C5cm8u(Ҽ/A.a-E41;WGu;_>22R+n*!X i)Uh+(q.s}|iWLmYRhf&AR$_N3̗Sҹ -y'odҸNnFޑ@MtBG*`jԽ /dϜHZ y&8b`.aW뻓$.S1/pBq]latNm.ݿ9|:%`0WBMzZScߏPGoBO7R#0:|՟cuoCaFw@동|Ǩw˶{%4*RYNÅ5š3@iQ=o.~q7)!81Ǯ>'0uNiӔBuş0{h^~ķ. ֩ |yڠpB4?N\-0zj^ \I:d1A;VH:{;5{֣Oo({ҔGim~cwT+Jc&xV;; (QK,ɹxJNP t5&\6B/I~;p D*p.4\eƆT="㲪,hKm'GCGJe?vuN'Do!hK *v,d\|Popm*4R4A6U-#6 枍+$7'(-DJSnq0+@|kc!bCuct5!kxm8B-!Ó[1S4}Hq<(%HA&J{D@ N wMʹR]C/ -8B&.f+0x0#sJR-6!>׿ƾ1cJ0*̓C(ѩ>y]~tP~d^息Sم[kbɎyl/|Ɨ0)mFøbE>l"RHA l?[Jg(Tt}LH\tM:EenߊŧaG9 Q6t<4)-*{䋇~{+OeCZPfM&V+%!MTQI#"&[c5gR][tUIP"f lQvUXaZPNB04")38PJ븎xk_ kU5ey\ CZJvBbƤ\P-guhZYG\F79!,(IĕweTT99gXyQ$LQ4a=%nL`A!L>}(M|S4))T[_?2TX%W@LB;Z"O4O\M0"-CaH5 #[tv #>~8UF@[/v_U:%s'8*=O.`cAd.^h%n{M9" ո܃mHPP8=hD/UF N !"lA<.jl6F%rb,+<>4=3M!EApQ{i _s#2ӋlZT>!`pكU(5Sƫ:ïv.N/I^fVz8z WUiHSzNML;U3|>=:FN 9cAQ[Θnok kҸS;uz0 'H+v|F`7j&>ʧk&̨<]5l${*=GV!kiVnJT,b>"UO3pR)pE b7 nYZ,q,ƌ1b^G^]4!\y+ҁ&'r\k^+լ:23Lq/h!`hI+4R_u3wx ҉D}ЛVeE˭1D$20hQL0Q e [?Ix?`詣4z{>ރP=3:r=Z^9id&^=?ג2l1߱Jj͐' r)I<5FcfVc:GB(i]H"7B=(z=c.ğ1&֑X5eKP  -3 ca4-k0i)eR 2  4-z_f)o! ҿ˷Zz7Yf&TRN~)7YDD1՜5m+^5#üvߖ; I3S)b:XDbbD }u1x{Cg(! =ij9$~qBp"u'~g!̎'|"_{A4쎡4C2F5u(87.R<%Oͭ Ve-4:DyvhۧR8$с@#Z`ѫ( ZLSGISpDwZL>~7S =)#0A-\u>*ʣx{l9݀clܓb=FDVҔզ$ [B*.㑢 v3ᆖ`'B{NO)_/yDrwGAsz08uZaSOuvy-D  "R HSQx]GQ\;>E-6(V5Kuɪ3bN(0G Bce3GJ7@v*2kP v§eGQf $lG@L0yAEMxp$b$\}B#Xؤ@Y<9RJ@?(^mWU0G檇t3>i( 5CXsI)$"afG0wVIVVW0LWg *WZ!@Lo]s~WQ X+^a ` EG2(-?t"(5r?p~ IT[S;:7Uݮ’wVP$%6|u^lCG [&6ڤЀv1:X+hI=b݂vS9^)Luaa5w&l~ 3`uwE M ^:F7~xnhu^ӈfqޑ<. i>Uv.DpsP;2`nq[ CDqG$ Sy,`r hR+*Bd'BHLG+,:H#pRMɁE<~[ Urk!s!H_ ]kE'Spj([M#Y.-u'}2ǒ`F]ê1A\59%6n桞Kܯ~'h:b}ٓT.SkyL,ٴn={Έl]`ujqZژB:Ѹ 1@:U` ^{-\*M?(׆)j1p5*V-=|| )jD\o pK*Uk#IP`. jZk ZRK'1kD `U}־"EtCjAֿD،3|uPD9X4T xc K~3B:Qm+$6fUӣT*GZ. >b_lPyTpgyTrVZju&>vhzVHySף@r(0Qj3<njCТFB[A*ڠUwҡkCqOЎ OIZ&Z7{ӆބhvD-S3(F:U;`@GVUJd%/%ܵv|/(1A_H}qD̠ ȴV$5hFA{AD; ( =AD"I@EeLb @%$O^ <r%@,1dhvD-KUMPcCز6(yPJvuTsV|gڕЃtjs3:>7j f% kOu^cd9#r$uvǨ~[KN9CȠEra 0K;ыDEp'~O*=@1Gb[`P_vH*w4oѧG~0)'i]ADG6*Lk[K ʚsw=c۠pU$6h{SVF:5CУu4]"ٕt. bZ\ 3PVEU亠X}#V3?7z2(D<1BUC2hhUO>i )W0O$4-fwDYS Z w6k(yM-ZpRڥ&Ĺ-~ܠ2P?t CNL&s&wA >E|ZNDHaF< (3!xZkG2Y/D-Sӫ綰_ !a#eb1P{QG kbfXx BL0@d|BA p&9+[<[n*TQ,S/!DӄՅM$V qJi^h{Cd;ɔS>@h5*rLZ}р)ah g4T|d8n2Q.n{m1#ק0݉L5wncQ+ґ !f[F%UW1EKa #Pp>)虢w}oyS}9vkSgwFdxoDM4꽓NEm) p"PPrR>((IFޥZ[vY:+Z6LaR*ܯ$FmdUN fF1*#zn H1ꀫeQLmz&\'$+&mj9X&?JZlR - ]X}%khRnR( k'`XlЖd=>nf$=EVr Š2}5n BHp$QM$n rݒfr.NġV֒ U"dÖf`צjk1 Y7Țz6qc(I8A48QIϋ0}YT,O 0_rv1"`M8*x>yMA bȂ C\(0& @ES2$BܑGRr&LkˤztH/&b(|\{eVFW]NVYѐ1A=,r-rQ㴭Ŵ^9۔Yc%u-ٮ\e!xVx臨P>`HAH2 Lx,p[{*ڮQYbWUƇ`Q f=Ώ F`\؍vI;3bedY,*?SD 1 LRW)Z9=jm }k톁Q[&IFJA$?z6RͣN%U# 63bP[/犲gkגf#6>D.BS*Ia&SlD'G)gHS{v6MZc8j뛎P,媾O1'-N5*ٿv$(bPq$\hmsUb| 2w7Gd`: $&DX <h\`ՆEfqd]j),"YiZ Lڝ qƀ3TaX}@L {9(R=:s6mJ/A T6ܳVSPvO v;Qx`jR7b$ӧ嬲DYĹ=Yij/Zl *׹*}v /VI= $cÙ)W֢`0Ek !IfV <!Mz#4Z܊[lX$ O!#DZ=ɣ;pq̓Oρ74#ǍRm+!ՎRa=nRhsxVAYnw 2` h(*ʯ=Sٸ`)ɏPcR4 X=R lEU<& rf}PErwGVL-a4$ҶmŚeX$O+9yx״#AakAQEvrCmQZ[z8slZJIZuٽ908o^ іABbPB0yjow lk ZcbKGD pHYs㑤"tIME  '9tEXtCommentCreated with GIMPW IDATx{P?{a" #Y^x &SV$UFLU1c&M;mLIMvzӒKIL3$ĀD%X 7Er[]}E r>3ys<~9( `JB! @C !:អ"B"#GT$ +2=2llw=y)S]l =^ ijPyyT<؍ 0mq PyaSϒs#sn^XJ %ac0B! @C !0B! @C !a0B! Wvt8&hl!7PG|[6VիQz2¨cܲT*y8 492>r1!an_t;rk+U>z+k'"1[kke0׿BZw[a>I]'|#i2ٽ}%K/՜\cB-KN C %! @CpK>"X+1WVR[Ǥg|N@w56RxWA֭cƍ>u$'^Qdԛ1\6=qL>W_j([˗w%ac0B+94w. i)dVpu44px >ɓ}/{2 %}\4H/oXvJܻR0yFQtP qK_,{aC@C !qOzWS6KOlz "3Ǽ:Pp7cC @C !?wf`:p++){A:0BV$i 埒h14260f aB5T?ckt]&ZF %}Ƨ@L7~<J%6Za}VJS۲={|ʈoIZIӧ34!UnGazq~IwP(@4MP8Vuu{{ $0 GXs'z=H-t57  "1)dn܈>.nkJ ۹駱x<Ӊ!.0&ϞM֎ ^N%G?CG]^!6Cl,sׯ'8:S4v;MeevtDYD̜btbAK2wFj5-UUҥhR##xF% ٶ@[O[pdLNdfR΁'Jn@Ma!IJ 쏉i11nwi'ț؟őD 32(1=uuK wUߑ^RIW cje:c>1֪*Z~{vM/OhۺPcs_ð溺Ams^QQ(z cc͏4V;F!&ɫ7D(zymZlⳳݺ*JJ[XD1cә $l@H:EFޒ4rftrL{U瓺o@{{hD9iG1NΟنqo6]m8{#p?neˈf_O?uZ-^OI%Yћ{y-^QA~ϑz{qPpN޼o'پ}cht: ؁S^݀(ӟ* gXc#o햶9{ޟ|5? +V3IYJCq/顇Pi4غWD$$>}:#U=+\O| ,@٫?>ۼfw7Z kj GO?͢^Q? ]+JKk`]^BiZSԩM**8p!ھ$Ú5,ر;[KUMM(JFgxꨯ磕+;7g&,ݞ@G\\̉rw&lTL8v*XBb.˾yEPt8V^lv͵̘'R03k@#`%"1ϑX.(ƍDо{7Iկ1.zu3%!zF_~+0S#8"'aLѣ߰/8ZWi&ϞMTRRolί^=c5HVZpt8\~] ]r2jK{;͹8JC@Ut8,ʹH2`\T(.`3@$ݎ$60iZQQhvwS[K6,oBaB-zjki۽{=' mX'a0B! ?xSe{iHIENDB`lager-0.1.1/doc/_static/modules.svg000066400000000000000000001733461451275761400172210ustar00rootroot00000000000000 image/svg+xml views models reducers actions services effects users search posts undo comments users views reducer effects model action service search views reducer effects model action service posts views reducer effects model action service comments views reducer effects model action service undo reducer model action service Horizontal modularization Vertical modularization lager-0.1.1/doc/_static/mouse.svg000066400000000000000000000633101451275761400166660ustar00rootroot00000000000000 image/svg+xml lager-0.1.1/doc/_static/patreon.svg000066400000000000000000000155631451275761400172150ustar00rootroot00000000000000 image/svg+xmlsupportsupportsupport us on lager-0.1.1/doc/_static/sinusoidal-badge.svg000066400000000000000000000771071451275761400207610ustar00rootroot00000000000000 image/svg+xml lager-0.1.1/doc/_static/time-travel.png000066400000000000000000003032731451275761400177610ustar00rootroot00000000000000PNG  IHDR8gVsBITOtEXtSoftwaregnome-screenshot> IDATx}\S?wrHrDjr'L:u_Uk׬NZVvjݬZ}Z+])lX rgTH'? 9 z979ps|+W$WNv`fB4-<,?;'&^ "cNG`/i2H:l0]\ ̀ ._3@4- nhp ɮwwMv]f2_S NPT*"XȿGX7+Tռu査ܜ?LU L{ @PTOJ$ѳ$IvV3/>ၠTFT,^7OH$7>I!̀ ,MzNO6&)x3vWKDDbO6ņP[mUնUvgn9g.,yz /UP{ O=?pH;FWiܕK# ҶvLD+b޽z7gk gS}f~׷w?Hd)8ȑ/BBBBCٍ7@觩Is_' B5] Kj6P[ɏ?TӜ/r^?K+_-o˞_3KtevZgZz/߾n5fwS#}d_i3$i!͒|Yh[{^yz[=G={>.""scu[~" źukde2/?kkr޴UtBdҘ7U;хG^YqWy\tonnl;\|4񍫼Ͽ/p%_8O W-y_)!Jrps{}qߞ3b "ryCD11U('zy|MWUf*_ȿ_`>ooo!oYDq *}E̍KZH_3Ǜ 6 T({9jyܘ .iU;W=nTo޸$ɻed)82""G?D1sZ +3,_P4Xqӏ,YMh&Y4>Svx--gຒ{{z3.F9I{ SZ~]V׈OWWWOF`C &[:㼺ylzz{sV8ϓwʒiNd,Q/jL pάo\dVm#gkj5ϗ^UickgnUeg\ro'j^UyE%+V.-4zz)00`[㓵gϞI@dk0_e"N(<<}HeOe, m=I?lKח~ch=63a{n|/dkN7+O lĭE旿|azkKO\lU@szߊmUu~ϋojÅٚ7k+"2ϾoOQk<1ze11}{_|qtr+ p/_ȿ%_`IrJNlNfS/sx3I~)Rʈ#GݘD"Ys7)*ܘկ cNG5Qȿ*_`L`B1؞I~)UVVVVV6,^`[^]]m/>s(bt|?N/Ξ}wdWNV)8 hfkdހӘUo)LMNV) '۾G~4^{~ϋoYw)LMNV)q_!Ad{B4e'0 h@4p؝;w& _;Ux -4444UdW_SɅKpanrz@rHW`Lȿu 3esh4Nb}5P(3r0C0 thi~\>[*da^/H&:0u!M ̷ߍFH*y!pP(ɼBBXH`4x8ȿuS~#g;w=~~&ӝɮLQȿ u ӞMAM*d2?a@0M!DhiD"dW`@d[`7$ L"l<^Xau`GTԢE\{!񊽷ꀀήɮԂ `&qL|D"nh/)9 [__o̼ I$ظ8TBDIS^^n^@RTGk4lnCCY"ڸyW@ GV[SSv,aRR=kUzzO:m0ַ)(.׆ yx=D4Dib뻌80"Ɯa׀w?55eY> k4Sm Ƅ%CV`LN6@͝;#&&.RSSqG 5k8~N eb'gQh({cF즦SN9esű)K;O h,o';w|;wN.|nƴ\$7,)h2QQU\$"\_/9QvHDmی^rD'JDK܅a\.˹djjj4Vj]Ȍc+}oJx444$2ϯ֭A"ٳzzL^=mm48A4h2 tG*?}|cgG7n:::ݔi4?ȵeN/__buD4Bqm j*"jo8rd_lF\\n<4?zKΖ1ʣܓuK6.]8ן:/F^sOƅ3ۍW~^Aҟi /~@ql{RVԧNT LzHF݊뛸7 ߙ?wVuu^i4#G~kr0G֬Ys< ]L%˹c̸sEEEyzzpӹܹ2w\c\\#APHd Z>?R˶TTяn_!@87&XOc` CMh]p5uΙp\I$hsǰL*榎hD3l]XCFuI10b""꬯lS;YViQΕt5r0emQn/ALC ìXhӧO'rc[`@C(DG,ssBD<)!r/;u@\Uqj׫T~}^^"(""B$YΝ;OWVV v9zCaXln,7@ryPSRvVFB3o¯o]zuO--Yf<8 4tyȃJ0:?*)7 sZ}_mQܰt{t5^J5vnɨt6WW5Wd,˘H-T!K$>FH"f6{1. 驨 ("" %ѲetÚGEEi4M55W,s,%*j1jjE(47rn-ODIɩ'o2D4DCD$!"sH`>[[ZT'C4Dl&͉=>LwU,r#.^_ B /{˿ A\*.̼WtZZ[*N9u$͙3p̙3?1Gb&;Mch)55E"QԩSVsCCٴ4z衰0OO ;▆ӕ%St K;ғJo9e)V,H#LfgSƎN36rS?yFҫ.M]>[g-*B، 0X>x%*>>>[6M-+XND\eǎ3g)\Z=wܬY򠄄dBBԸoKDOQ\\lIuӐAI(tD&A/"2ɂƾ>A`ȩ>z}S뙷aBeh"?'ITlHD)))Jb/ܬq_4/KȿC5u<\%qh3,]\\;rז:;;KЭ[$OaAD}}}nrt[$0b͊K*++A39OrV6t:yɈF9~sIwcx.#ODw6߻gk͎Nbg\Q",xo['aKS3zj߼?d6]<2*ٳW(|*.t;:t!2Q gPwSu1⫯JOOj[dh$6Νd򠁁B655Y=McU"jl&,ն͉Fs߲Nyyű,:&0t>D #h]b/ァ>s&g?S/'o'!y2:ooADGO&X,nT*߳#X'va[F{7_ #<׹:+3/;6qBNΓ*ʝ%ܦ Wo cf q#*ܤE)NK_Mi#u^rK]nvWލG?tS^loӶ7:x6~Iqwk pw'dEUn#Qgccdr2U:o_`1 ǎ;rYȑ#EEEާN>rȱcTSSSc0XAbEz||<ǏR ;3%ٔUVVq.[|\"%R!,YEzMAGE>ߴiKDD4tG"mw}MxC5c uD\%qٌ6@T\pN*!P6;IMMIIqcX9mmmn[u▒hADD37)ua' n) IDATRղ~zHDxRcϭ [$5G r!,G-\s[Z4F6*`{Sy&cy\XhX̧VFV}NGKR.Yp6Ѭ+?B"ꩻԨsg%!ܪ=ugI؜9GT! u=3mKB#a$d?I&fpR)))/˜1_9;͚5~dʬQS|FnUUzhhB Ѡ@  (B"JB.h B`P /Z34a}9Db0. 3S^]pY x}ٵ7T˖$YYY\W7T*uGc*))DE-2q醆khٲe]gn}_^?m|xFf͡>?Dg=nklͫ ];3C OKndKvsllT,2[񌿌1tܬ߹kMG?9BK^zh0x쉢8G$duު,t{vY?=oe{]IÙ>.FD=de,\SScƥm>>f͚o544>}U\\R%JX'_c*ndD$hhhhppH@$ DCCLdY\U V"'7fe٬L>K=A͐!// !ss8M濷=e;+va~w?DzOXsuϳwƦ=oP=|*g#}fXW__t~R4el/Đ2S0+V|jj<=='IFD"hhVGe"$1Ͽug k֑H$DjJ˅Re˸'..*"eT~'..]>4@/@y/ !s8M@OHT`= Op` -0]{z&¾ɮ ' ͏X@0 DhQbqg'{pdW(_ɅiϲX|IZL_:]Fd2⻉ : _ ⺩c+.&J -DiX,J&0!M24@'ux2 &UxL8ȿu 3w)!prp/) qC qM'*3Kȿ/ug\?/] nqOW^zHIIǜ f*O@[ h@4- nhp 4@[ h@4- nhp 4@[ h?ܼ4U ^~~=7&20ʧE\v`zf4=Xߢގ )jܹcNG5e aȿ\2pF+|8Z:YDˇ>QgoiWwR䤭GQUdY.9 bM%]]Ii ]ݏ.sWߤ+ ]{gWMbY]/T,y%mK Ɩxg/<>}mfZ4լ(?\\ew9!^bc/+(?Rv+#}/,hrfM/"_DkJrnkNRQpTdWY "ҩPȃ:ˎk'6p~?M>33"ҕ.~HQgi~ .AC2̍d?2A x+wlR0#D&KCbybsSWHڜ?ͣdTN ȿB-Q8 Dmme~*%WM#"Cm~~C}ڰ}}$5߇kL⟺WҺ,PiߤiADD}BLD^[[}?:V.8 \Yo Җ 㗐XA9wBB]E-euƯݰR&V><OUAxu޾mO|9:w[w; +sSM9׃ j2&_n2/^倣mrjxx'acv7"K%#"åV̵1 *7o3 6M a:+ Dº1D2Sy32=]AD+;>*T:%YDr^^A ť*< D49a7V0DMYAAFLLy= N3ȿ lJ]v/^倣\-_KE2]B["SoWAE2^m7g//3n7E}m6!߿.^5O+2uԖ<=mM[j[jwyO]dC|]juu%-Fgdg&)\7:ίX"鶼fe.}OMVJ;P::dӻX}7җr zO'I^ӳ%)/VSW]WXw>ʧwz-4Sss#XCuͣμ!÷^!k3׋ <>˱%$sos>!9I qsGsҢʫ6ov}xw<:)9IME1k,URޚwp+֥ֆ[N(0&JJ޼\Va+q{)ӟNWԥ?rϘ.T2~.Y)'8o:{^/nHkK{/x%~L7qBSt[v.Fjaqt3]g; /}t}D2"eU77ki4Kʕ1ܖW _e+tYeB\RaW5+;jw cL7C#oΖ Ǔ#1^#eK5_pH z[nTTi-ŵI*k,sY_-U<oKS>X)w\xkn5HHLW2"U[?BŒ%1`_ߥUTT5Yw<ޱ2ًf^AD+M璒bXCC3ܯMqPG\Β>"Yg8\5o`} Pff$*YQSE8_ȿ!B /^倣\QAG= O{yR;`^#r?re,$%xx ਔU2Kנ>V< 95wL̼30ڰu|^6:W?ԊF]eSN!HW0[wj޶9+rD"&HJJ޷sWc2땝bx{^jUu'H0j,!ӷl[`1b#UI~)O;RV7:?DF 1?5ׇSO^f""Zq-n.rR»/:Z2Pre:3ɫdto ~K(sv7|d/ԪyK6vǁe{mgP-YLZvW,]H ")-<53я 6AFDC6wg._2n^l/>uv<_uŅD|GyVDzf_{[*\ٝ#S%X֗*!2MDDv!-_1o)$Ɠži[~ID3۾HDFQC]9/mJcbק_O݁-^wH"R}Q!u^.KSմ:~mԙ1Ofp_/)7Rcb*򱂮dH` w!i3:҃-4 + F"6!"SZc hãLk-]F%vQOøOȼ(6IȿϘ.7Uwr_~!~]nBF {Y&uH.sܷAY"֒|gnq8t|3Kơsj|]lˈdVEDdP}#zR_E F_ľ^|zo\!pIyޣI`S׍ U ݆~H|L.\iEK?8s$ضY"گxc0|yo"۠gFy$ Pۚj%iWE:MʏȠ)>pP-qN_rn ֲ}*"7Wj]Ҽon*A /"_ȿ҃GӞ&F )yH×1#ur$KabyDF3VWXb!ov|WTQ^R]^bYcG]@~o*є?LjU睈~WWܐ@DAl/`ZcX`aADdj2f]sP P,%M$M+R8U%8gs].eJ.\`¦^mT`8F 6YFDˇGݛKƑøWKƁsj|yEMd j1), #q|9pV}]Ӭ'('M-eEu#e "EDD]էOt5hE>Ka#j~jDk1 |L'ՃDyR"of{;I`sJ6.u,ն;qiEd~\ix,zF&d ռ{~,ZTJKvkN]W]%"?QkGQ]^IAA1r~>. RUDlt)/_ȿ!BrA.jDDD_C>>DDoQ3oe(:82vFD"" K7Ig+fK;do"^s5'Fa?5㕸-5Q=Rط^!,bmQ]d 1~g }:MH;|,e$<[U "J!cȳ8^9/??ଢ଼o{٨cqWz =:+!?M+sIZWqh~pH/CDFc,$p|k~ҊiJV.V].bY fȤ.78* [1D9?c[]D~D$Yl.|%1 u珺.dtdw8t|3Kơ|6mˉuy}6d1/~EFjw.K]I>\tWry:]IHjж 7euk; ˈt|c{5m!p0  DR"hNDDzCejg7j To.T4t#iSH[2|]ڿjq[T0ч?|ﶯ<@`o$?zPXg`,~V[k[d|~DZ77)! /_Cl90WBHt7hw~WW2q᫣Lѫ֍Ӡ1QΕ%/w57QHZոb;oYܢ_0tZsgXǪ.">ugT1txu6r&)5=Y bHLڔR>>*|7x99h(UưWlbX0kh IDATY9Q *6@DdoъDDBߜ}֦sh^{%^_qMLOuw"rD~ &jmDb__Җ2{ڪ]8M(xfFT7Y7=v %[QR7<ۧE#JIWk'K)+"3+ Ƀ,|c]ގՏHcV?x=NSsoݪsAj@5._w! (ȿƬG985@DD"7=^"S˅o#s;dDhH$DF' -7jk9luågnߞ)#Cw7[jFD$c j 2D f"JtjZGTxM$* ½wK?",J*o%1u?~IM ER"ΡcWΗ~;ϼ>"J-*Eb7OricF\B6**@DdjpxUǠ8|p4!6vǡeY0Rf)(32"ҕ љlvg${BL֕uqt۪|ٹd""{t%Ul,mE]gG2Fɍ,W*S1^A!ē&1`j&"?3: KF/_֐!Bů>|<rq.jַh'yQ@T"my?*JnFm˒"f+uD2"X޷&'nFzZ5F"MD"Go'FS z;n^BA$$ r4qtf&7r(.P`vo5^^>!&2mla(W&&' w̿n$r*YF\z]]Y> @ʴ̤>uPZQe֧v19/>VwɕJcG(X2%b3^wxU3Yu]>g yumSYAaydwè>s7r\s^#Zd1ϡc_dԨ[LX<2F"%aэ&H(/ 3 QJZRsKOSvX2d W۠C)oH9|C8Q" "bnȖqׅKƑm>|3K9qk[ID$K]_vte\e+,դe$K}%}kF./|5zL#ck]u3r7ݍ'ǡV߲D,"o'ܦًLk:"g"Ifcg>][Mد0{.1Ce* ]y唘DDjoӵ +Ժ>" ]}D "=;CSڥ)+/>%x./T'R&GwӽoVGdnxZ\*OL#"2TTT /!"BErqꂣb}oU ΜìG.$rxAayCd* j=M:UD~Tz.|ww?H#^qwKt؞.m0G4+-W)ekקQ%(V2̗b2G輼#Kպ4HR Z; A~νj>VNQ0İ9*""RV3Sly:! R弤2L\ʧwz$PsH}Za:><,OɌܘGaLJW9|^WXIb)Wj""Cg UX8FYF͵ˊ|yx-7ZLlH|R52g{""M[J'Ck|[26h&6襣Gd*wW(bYD1)垮uaqt۪|{N-Wc}޾˜-,ði^K;Xwx߱mYM{޼R޾.G!W6SvKVu9Vg8x#a( B<8u㑤[~ibI\ȗ,X22^ƿt;1cFbM3h [E^Ik#d9/%L3 o{tSGlm%l+ VpruAt gge //a__BE% q3/Zhs#9hܩ-zf?>H]Wr[ssE:ޱh*{6G 1G.k{ go('zϹ~?wMssMQz%ZmϋD$gyDfVZw/N텂EK\`H *SL\x9ڑ3Mz߉Ш$o}HKnp7iWת\bͽuOcȱ%7?7rˏC#/&}_Ė/=co/.|\-o)$"RzǗ&tz/b⚊;̠.*9Sc',=':c͊ud˻hjzs"yehdɺGI\bٖC<ē{OS/c܊5_4DQ{S{ؔw>Hșw_>pP<;q_tĥy_/ZTTpJͫsninHOClt/'ẏrnɖ3Hjl`i軑ZM׮!_Gʛ=ZeΛs57<3o5_;O2SlҁӾ eN9JnΕsblބj|FZwyîDΜ8y}Gthf8D>l{7\iM9 ]7jgZ|٫SOnZ\.׸SQ L F?kGS8p0$Bw_T;u\NJu6+m{_lon|{'Vc-/?7og,+~'" ͟i+.?ɕ]{=΂Ta '^` ųޕ%"s87'6CX""nz;%wMq?J -b}yi|9ElS8WX'"uao;8GyX]`nб-jȹ{S Fߋ7f$yuڨd=/ u t .+wQ(tYTuYD.>tcJdo;8G3ݣ]_~n҇}GZFS;R՗^iˬ %d=/ ufK]ʻQȕ3]m-u\ɿMp3B ^s=3LzZ<;;{~mƓ)ѹ0PH/ ̖g@@Fπl|4  H :IA4 )$t@h@R H :IA4 )$t@h@R H KW+kJJJ&ܒ`2O~I7q? Lxch /Onƿ˿LW<F3s<!T_"rٴD?3G4 )$t@h@R EdhTKw,`6:N4j1Lt2$EZyC4&뮱Rh0?"t@HlFɺ@ɺt"[>FFc8 çNuhZ8Q(֢2: F4^J ?D"i h">sLWWWe-}lfB3GpH h4-װH$ICo0g(ӧ;2DΞ3ʛ@<b Ez{{QwdL(6֝IGp8Ģt@h@R H :IA4 )$}mGOᣂRfMw@IPZg/*=GULy%۾s;*hǼJD_z7+"goܓ%K:d,?^|Aw1܂tٚ ̿8Ns™1-iUDv]+ [))~.t_9ɵ?@T)Pzcials_ LCft@l8{ɹh~6O5N=(7,o{O58c\Q>\$@=s6x#"|4x'O).]QltW^ɝw JѨdX5-x R}Gl@EH2X-WD/] ^ ]\=8^:5|Ue-'y\gOBj ͓ r'tUrHRXhKcq)**k>SQRgD&t@ϙgPϥHo%mD/p46)abϳG/^pB]$EFjb&ҙ Yg6oig//@ ʀhf#" EE],"2fS&d6Dҟ#gpEGDLb|sB]Xj73# PhtYU%k0\Yy2h4*2 F:6/_D?"sΝ_/Mp:zu00妳݆Ƚ& 2T-朾=e^I?,QU[o--1!BQ7z뭪f\MEH i fyn8/lqS.f30O.O34u@DDjdwn9Pϻ0+LhtFNUo.OkPD2^?HEHabNU~mKs FN[禲?=& }h¦9%7YDD^k$=ӣ̱͛7 ۙLfhTLl zr__"+ͅޏZOqUV8GrBsID=ElɝW|vՒo\:K͙ųwaX?Uioɷ䈈\OJm99>$dx`\Sg/H6͵zF=EDr +mw[̛ͱk Kv\G\*"D#"V_sN4X|EWE^d2eA?_"IЦ ^b>Ο#E`xxao.^w龜yM\()a˷Hγ C?\9I &!f\oLѕq%xv]?i퀎|q>=<:.kFqx7`+xoB@+@R H :IA4 )$t@h@R EdhTKw,`6:N4j1Lt2$EZyC4&뮱Rh0?"t@HlFɺ@ɺt"[>FFc8 çNuhZ8Q(֢2: F4^J ?D"i h">sLWWWe-}lfB3GpH h4-װH$ICo0g(ӧ;2DΞ3ʛ@<b Ez{{QwdL(6֝IGp8Ģt@h@R H :IA4 )$S|~'/Q/ KUo; '{Ͷ%t SLV}e&/Fv?vuMwո'~ՕdxI9O67 +@@v': ]ߨo;+,aF deQЊáJྺt IDATHzi(8`QБPP@\_ 2ZBf;/j'$)Q/!]t%qL4WDn7剈H-{wEOslق 쒥OfkYzǢ|tG mgbޟWrv͕OV ei%kR=S;; @*eDtn8 ۻz}GS" @>";Q4Ɉ;y۶?VUR_ 2zH$  BFu@*ѴPَ @fu@vU$v_ 2Z9IFR : TUO\_ 2ZDD,bMw $;rPcga beTt_{},W\uҒi3 Ж8RTTiC3":a{Coj+Lb blfe3IC˒1%F块[Iw%dUH$s1hUbht Aj?t@H%HfuCJ<֡7n,W+->;a#-˭}px]pjXoW_ZK_Ab n0l(q vQG[Au,MV,K44mD )NiY2q㱧;d&֐M`\fAAu}cg{Juӳ#ptMMEt]]ZsծeEDg!'7}v?_s cNSc'$U&7=;6_Y4D6;iq iYCiSR\U+*]j3,dD3wQ; N l'f5d䦏#6>i2y ARerӳslCEMmӸ_&7}ggVW@O-,&HcSZfa,7$BAuVV*͂: @ #m@i@ ٳ[\l-ʥ~t]͡J<={2{>)[UjU ;99YU]r*z׸1R\0Zv ko:m+k]NCу}G:YͶշy]N*}-ݣ)[jGlk0SoF3Y-Dk22S4V|r܃ŐZYi9{;M/ɡ6E%yM)١%ýS'>:qg~~wΎ]&:ͯ^SxSk@/^V3Vd-Yk7RʑkօK\sU-޶rIaǑ%w9[W]K^|tJ^詹%G4Saym.SC6mF]C -ÍexN^nvorroCi˖/Y><V.s05 Yɧbl4xfp`|2$;$(&Æ wz3uʫXyU;w9D9Βs5x 7LxЉ "6jWn=zezzLJK=\{ RvѺ΁_oY]]qٖZ+6Uw&vE$:]AqE4gp_w5Wu/X[btG6k# 1[޹U/\<n;m]/mtXw#;Ȉ֧ȑ'I~]Mu;l[XYziYzbWg|iAŢ$Xko<*AT+ZQkJqچ.eג{ IDGk^}׆5^޼WM 4]D,"[#X4k5{6)k,2n ]e! (;ia0`ʴKC2`%h_18fq:xt57늻c6*`(8"w!D*^XQ0̩h3VN†qA]bUTsgiAIҊFn>֪YܕVOGZ,v"ٗIDzZpįzu16 eIWp+(bwnm?fRz>-i cJrKv+̀ 0טx8m3;zs vFgô\;сk2k(JR_#8N$Z|AqOgΩ h6b28q%c7J>!k46cMopˊVEDTRxtaR39=Pݲ9sKtS=꛵&(֢2u5Bk8N$ xBruċgyѢE\Sgf$zx_]Tsǩs}lڑHi-.5O;-oaSïGх}+?zg}'/OBhO-P/}}ɽruhJqE.rw|mS-hO-j| 7*]g""Z"{Oľ3/.*o 5D{.Vݺ ?'p='#Nʱ*=Nvq;~x̝op٦U]Hs%K9Ǣepz"?߳zzczjnuI瑃1]jyM+Lû]\G_{Ǐ>}%бi>9ΫϏ^}O䨅Ey>i=W]\>vR\Zdœnקgb>sjh4ڦt4ı@49zٽ~"" ncՏwdYW X+$w% 57eŷG-<-a39>PM09\_; TUcgoW'H  kBqG.^ed2E' _}sq?!qI8,L JX~!ITcZvg1  ,[jGL]$FɚZo&-iuc%%%=دo[ GE"Wz]l6WUݦ(Fo(O~\RR2ѷ7+e\էgr沚%4Yd2CDk>03\1W>ݑ՗D"gGMH"/9rt 3S\|5?yV0 /]5So?%OpHf)l<4ѣ5YCz{=W8-\_@8;SiGCt*ݢ rX۞ĩ/}HC k_}-!Q%5a&;:kh.麈蚖ZV|_,J,i( y ɾ#lM"y-sfk _b]KUXiCi8}VVUD+M "PP@H(SȕOG[t fk_V@-.U2Y E>xEDm4B(QUU[ Q[~GD"'ӿbvZÌP̈́KUUP }ap=.xC,cmRˡh_ݍS[]v9bCMݱ+?j~t]͡8,Vm)}854) ⴫"xzp}=+"y" *F5/ա  Y';: i;X6w7-ƣ!Yw"rݾm]{=nOՍW_j]z7pV'fϧW;ۦZ6SEXXU}zg;}M@_nh:Yf *Z[F|շS2oH@UDfGBS[eKWT;v]Oۚf`snq:]^w%yf(V}/LD]~k믒.wYBƖv98̒} ]Ӵ)3|z%|[uMk[;uS[jWU֑0Xs{D?vۣA5VšϹmOB!.YXw^Y]VB۷6d]ӧ7Ď-i"_@7/!@BYsł?c+rjI‘|L_3ZY,ѻ}, uq8\i,;;$رŲ/t;ѓlDQhڵf`O/[7[s\a樿&aB!(x6 e6IwA]mCW]n=X(X쮑UN1B.bGsl-mtÛoM=T:,(6SC@PM` DTIR&kNVz6>uw}_`Hj|q*?/wQU卯>i4];;\h|f$QfG""bqL^<@\ z_~ A M&f B8TUkƓL?\^:P<՟,_5C^zj݆j_KOl{깗`vSj.%ǜ1slF}!];>(PUE>gH ꯉ">MjM4 W@'JgF}]3Peϳ-{Fod|}^~DwlD:a,XE4)ꯉbɴW(f+:T}ƥ47w64u 0ee-[.(Zkc+ Fr4lߦYu.1I1Ue~Z ez Nt@#"-NYokϯe$t@hIF(4FPcg`J?TG\c2M88F L1˂ fsb1l6tӂ?MZR^QĖM@V"eIؘ`hTL&XJe-D"tG5!\Yy2X+EQIfE_u;w; ft@g5mu=JK`NwXH,̂6[XoW_ZK_A* L"#TU[o=sY=b-***--Z̦g>PZڻ׶̭N?#nZHMD,&j!=1)-0 lax*]耾 L&S4:r#ժ|s75(C]u"}muZj<+E:H0at@g7]EDt]ED״q\+xC) pjg!'hۦvK|<Mo=65}jeUKMq|hen%֝YwdLn8bKqϊdh4F6u~U-PD yIB#.؛mcZfa,7qa a2 >1"(8뻞* Y.дow ۏ٭vGo6yy.o/~sgh5m7?o6Go O}js?}UZ}G}{vlXSr(Zk={_Kf!fe6]߾vQߑщm+k]NCу}G: fm^ӡJ(_Ka}97F>w\)|dWg`~|SڻTqc6 nVw;EDo~g6YJY[[Ძſ-mwo^_>QM/(s]Uv/7]H\B?ޘGZ #:Ec3S,_9?j o>uZϾ7jM Ihz Q[]v9bCM^<5-b|WH:H[2Et_h̋-em:O,"rӝGzͭ2Cyuϡ--:zFzz +p51/Zww˃zjn]XK?9{]]V.+Ow:Z]48qxo[Y~k9jFY~s+Ϣ,Z.Lk3Qu?e.E﹪zo[HKLK/W9sz?:q,[QH9-#FRaym.b` 55ޏ7ıhe9GN`l] ~rTṘ_}KR<ֱGM9y 쁃 1lmʑcwכu6Q=8zy?za -ÍexN^nvorroCi˖/Y><V.s0%jD71yOqsVNxfز!qjڰNUU>| ٽԣ˵:\=Z9Gmb]5Nq/ v_-WmiDN% ^E/0QZq5Nm{f25l\Rzreօf͆iΩї5 .oeɞ Vt6K.6:,;{ǾꡇWO򖶠ϯ] C"38l5]1b ΩѵW^K[xz 詺rHSg\c34Fި?<u[]9bK̂3xfٲ !`\v^Ym_/$/!ĀFXܕn'fwe"17F:돶ꢸ+X+7ZF.AixH@Uˋ;Om۷>46ʚ U^ppw,rZֺ}#wkYdzS6ȅ=+(.]kPOh"'z[yD:^߶w<ļ^nbbL6hEDt] zX,؜&YQ T%Ӹoĵe}>{-КiDǠu*%,"fwG_Cx}z`HnH@'+!.;=k+mMӼ~:NfePGPZ%kTG\it46iBiߑ}zpGz:&xs_gKSNbbFdN&w6%PPteMun'貝❱8/7f.wYBƖvyCzZ}k6 @]K=޾oȍBX̊Ux3nΎuN*mGP ("Jt(wmuߒNVb5Rk5V=kv۔xoOvmo7 l~_ \. \J/%LLڝ,]$4rǦv+r+R";Q|d}caɶ#t>M?|d]Dܛ\Bx8R]^er+-OЯ>_{JdW/nJ}۞.ڢ~4]<:\@P# Ē뉴7 jfX6=TicOsV 0s?o(*+"b.mbw-)/+OJ[pֿկꡧe\S)f("f,L 11cTT kfT^u$3sEQS4_‡{ީjj*>ұH\Dy x,)V9N} U4>E8*jg_ OmC]uE{|s+.[3Mo~Uxʪe(ҵJDBnR;BM5PopVT3 9[6IL vtnٱ*+qTV_+_{ݻg׮=ᄏr1K+le{B(}Yy*Vo!_GI"7c訪k*s(aoh(j*no+zُۥz@]C3zPqo x05T_]RGg0+< Xt]7ŭi"K80XiW/Hn'д^d<2Eh%5Q_Ҍ&O>xMnՈ{Nv{q/5'-5=u{³¾>3|;H'[Z5@_1hc'? Enm]FC8?2B>Z۟:r߻ry} Z:܍;nqm5~?T^<@K;vr-mYݹv4$bw@|Yy_iaz߯sx6`G#C=P;hѮBUS}zC&L$IZZ3DuiB̝ pm酳gQyχ?FHt"B( mD|~ƾ}x<~>w>LOfdf\^Gq_=G?z9ȡ)hDgn>E?y"L?+RE-bu>@QLwݗLd;1)"guC_ @O>XhdqT5|m?1< U5lwk7';?r9s^d Xɞ5%.9UMh?}!emw)fij? )W`&oj?ܼjUǻ3C9{b~`vyu|;.4EQ] bl >_z"ţVNfxT7FGG 7QUURt:/eJpFPhL#Ɣ[ Hf0?BR`z+whvyKȅ {A1Ӂ0R%{5-NB{'M7?3Q&k7*It3r hP4@csTzjjj=Ϋqzjjk< XFGG.!_gLk@X5v=w)"O~++H[L?1TZC lJm֝9.!8mC{$ďDݪSm~t8צEK(GMۃ-~hHO81vo}@k˃x~ JCp7C7D0V4O]vk=9xyRz,1y_V,` .oaa"iiKpZg=BˢK0LQܹ*bKUl@74̩NĦn݇U_榠O+ı,IIC gsjz7a 6Ra)bR /AO"9i"z\_e{ǓH*|]EUE{Z[>j#koӲs4U1dL1E%/"("ֻri2#e*ٵʹNOe-;k>wϦ-O#CNHڴ^K |:hz"f<_18.o֭Ō9(Se!^\^*D[QݰySrEdhSR9kG]^_]~kPK9Ɠ46=G#}եժ]agmwݫaZ]@mzS5E4 #v{JdW/nJ}۞߂ZأZjE:<>e&K͵}M}~-޵ɣÌP"RROץWj>EV&8"(2g*9Fpֿկꡧe2̅G+t<7şsBEQČIJݗP#^w_Se.Mvrju+bF"0G欪%:04R4XTE CϱT I=u""ri/U+(kح.ŕ MтmQuOkkfzQ\Վ&2*T}y-oU}t]<[禹b'uͭ(+.`ɔIXo{}5\F;k/C[?ȇ3gyZjC;~2^yָ15Yuz!Oc[kO#p4җlS!f2");L&3Y&D h@竦>a&R o aيn0tE3&}@uQ* 2A/=hn:L@KuS4C>fҜV:_NUUDLrX]@sV74Mj RV:'W}}T=~r`׈xdf +pި1Als]ֹ.sg~+1""ƅxkDՓ%Lѹ.,bR*ݕdrtWwwzorNʩj몪s13׮k+k+ޛxu 6gt 4~0sQF8n:k쓇>+I;+ׯ_bMuk\ݦk<5Ӌٳcs~¼p.qh(کUbɒ>/{F7*Ito]śjs7<;}Ҫ^*e׼3;ܽk7f-_|s¡hMD{Kegttulbf,Nfb .CyovWull󿵞0Ǎ #=(KJ,vbFth`;S^RO|1uo_G:Hh}Pp$""(%H8b*֝T8VWXlz@P 4(EA ( hEd_J%KȉO6hEe~u㍕1/We-ٮ]sTzjjj=eso @93C)ԁj7&t:m]9yq8޺qzѶ\ggͶ]4]HߊŬ.eO6hEdfөwBKpw(ԽR&d9vvԴ=◁cPh PT615M뮻zkٳsRtY~鳈n&c K`١3@qlLfANSnL]Z 0ŭFg(:S/LFlr+zzBcA$hKfI&):3.iul!dl:3);Cm1COh.hc@XJ6)Y7]%W[1bLOwgEhkpVu[3[] X`lɾ# 14 ɾݡpR\[= 4 ;;~qzG͡/-Dhu1?9149w\s}}Mxiu=qӻ^컯wLЯ&bo,ai=;V?n\3ؖyX`1h8EekjE3?ݗL޸tޛ~=;ӥ:h#(F `z֝7>wX[Rc#c?}ɥ \\lh$ӯ/=R462oߚ^ʾhd#PDƆQӡ_YXɼ?~=^J@١ID%8d:^鱰Ƴ{[f9IK{3tDp| X0 sw"a_U,R3z)R6RffL[]Sq+p=OFR"v-~f5?kѵsb{͘Hzۛ߾6:%7͇le~g5`$B=ֶ@0JZՌ͙l ThHWgݦz ?#~y&ЉYDuFd{{ GICqعy#}^`eUiJґL^[]U9헗W9u^m]UUw.f2}smsm{x (mVWˌN[}Zg+)qՌ2nܼr\Rz^ELSՙ3k)vXf*1ػ8{sftWzcG@(O ТTnt$NQ鮴ADDTOT<1KWV*(F# =zªJj"nx{Mkɹ饉@rDDb4LJe", Kf'o 8_M#PYݚͻxɮ_&u0=-"PpLG)~O z[܌Uퟒ/K90ErߐOl5H*jioب=9&R#gώD,6wU*s1q11`[ og,.KngꝽ@V5N JPoz/ts.Z3DSpQOT0iJ+O^E).㺞oںU""o9zr89;."bȷK֮aM>z)9gu)BC*`@5~3;׭co6j zRqVT}{'#tPퟒ yS7V(ܚ]RT:VDD$5g%;satrL /0!""ειTv@h{>ssgouyŖk',遞>]7 U_14D5QUUD)i"[M"ZkVYuV.-;6WeQY]snU}t]c_~0Ig(*n]}όE6ԆbȩPM{dS$Mf tݓ {}n͕S-ڰ)<D 뫯h>=@i*OvŦGUnm ./9LLwrժv8LO_r\=}뙸p&rf/6aAu*7LXs׼&\hȡ!~alidhDռ^_WibJ*"r*nlQL.]wJnP.抔'.$eΤ>5*ʥ~By*k|°d~KGv:+O=S~gଷc?/]cT㧽/lÝg8*e)1ED2cEDdbLg ɑqn.5[+""ύgf3ɑ΍ `QlcfJk#R-""ϼsk'Ϛy9]Uqem5 ͕$۝ׯZU&5xo;Y381=;6G={ ¹y2n`a\\}/aa;THy_ٶJ,K蹛n&;}Ҫ^ero׼3;ܽk7f-_|s5]άr_0el?%57CD=f|9!)ۃ #g|ل9~a4`b B_%3"} ޖӃel? :rhﯣy$si4h~>oZYpo|<(l\h@ ( hXQںOB `% =1t EVXǷo>V-44`~]4[@aX]Xyha%]j%,'NBCO.Ϭ@p5kӯYXɼ(wLξD4=[]lr?_;w}V**-,&_]^ʾDsb&uh O6hE.ӏp7lϗ=XEe~vH1.VT$(X]laOY|ם:WKpyȱLǀ#hZOau5,#n~%}i IDAT61h߰Q}o屿|}㴧VWQ,c'onѶ}"Y]iP0kzK\"}b46=:y/iu='?s4Yw:g{lˇP+cqI5x'2Fgn C sls? ]v'f73Uп/q)"f,L "ɉb9.Os8>q((6CEP+D@4E,E4F J)Y]X`%Jc.iu!`9#ȩiu`yc hXQ ~mS0VW}}T=~@1@wX]X!P 4'd22]յ,@;*=5uՅ(L&blVrM&kdJ8X]lΚmh'Y]S\.9U 5me#=XzP%6X U[dե(L&9Ja,j\[mWy)0 Sܪju 'YPt:];Fꭷފb]^m0' Q+4fĆ*yZ:NOξ<3!hѓHVWe.5ox<)NbhS44X]BΞf#X M1Eڬs]*Cp,Pdg[uk@1#$h϶mq;rɲy,p]%1Gxw(WֶFVtjΎ_-|VX/K#qI`Y(ZTMU0t,ZiКKѓuF\$=VioG. M+p0 MS3VU:>M.q4/ͤ`FI""S^ šd"?tg]IJJIcឰacPPxTo*>dg?7#q JKiMB8rtZ.PV "S3]IJOIt_ %lûEDg*W~)=e<k@ Ux&T{j쫬 @(7Ffؿ-0pp}duiie@9d~ ~wFH>8!"o2B $؃N!`*BB 윾t[p~vk34bumX][ȧS{^s)Hyhr>/.BgsPyso>ё,z/}nĞ/:X'ȯ8z0 tnna"ahnZ]w}OtĊ{jW~es3 SD 1MSDLð&KY􁘑Hv=/y,tniS~1 c1㡗N.tni#ef8a̴5Y@>=OFR"v-v-j}^[5h$C#3lO}Y}3W,]O}UOyEW4nkm4U;垀REOᾮΓcWUV׼z׭F¿9~?1+5I@D[v?ȱgS+ tF;~Hy퍜5n*zt`7l*.oMm/|nh>~'@ˎF]G[F<O*>=ڲv7jp(٣z};u͞yϣn)H_|G^Ny ""{]H`fEx}~41F b~5rUŔ)p}fD:U՚><4|%ᐦF_;J3>?ǷfOWlitIȁ{dzʣ%cGj޹ͧƻ?""b>ʳM<12=G:uOcW1: Mג:9Mfyt>/ x%78{2"'P8M+e-ͻI^w"P&lUil#/UuNξ t!8|"i"NL%Cw{c=Md5< cWKz"hlV3?&%x?I-]tF9٦/;M(+ ㌅o%g".թ*"W \p/|k 67k[>l:ƪ_Qi""WsDz=׌P:oܸ1Hӥt8޺qz F]i"jvGeMp=͛Zv}P9CO)"dY|)L&#CX8j0xG(t3h ޡ(SmL&cI1:A_*1@?.؋ZJE"Z*R4=2t?hקⱤ[Dh;;jj*>sk_̊n5M뮻nAK:oKӴ`AZ֝x!!U.}81!r9ejvBgͶFHr!'ux+c3k^ɬçB---ՃZu_1=>|r{ݻͫ~B!r7C/ZJD(rO>Twpߏ_XR6-Toj--*/}0Ѕ1v>8007w=Ot/رWOtM Z<9j.h`iHw ;d[d`8&E.3~XOv;v3薖G? Gt|uAˌ=ԕIv+""疅ƦֆMp$j(^_}eF]Mo_ >+p' be22r32tX B=d|`KKh+f2 9|PgQo mmGO v~u(-P*w㎇{_8a~-y{M]OC]'^bV Enm]FC8?2+xXطgu|}y[sSMN1hC39է74i"DTl6d2SJ[V.'.(qz੹`G؏KWl/us±2x+sI%=,qF;ڟRnh9qc`>l6HFD2n=ƙ<51%cؘ[r"N몶j]]]^oLx2tm8ujYQ:W++RFҕ2RKZB )  {x{}?D~<{ã8>zq3_ޭBCsj|'>:EPABd&W.H bŵf@ct.A!St.`>O}/\7W LLT|Gu}ˎ#,Xe@*PƦ`Sڅ>lB s7vyoj"or.6!^[}k'"; _9֬/C!_9s513oYo비j}>k\iN `` 9#"!Vjk^Vyz[%l-1054"}f@M9JDG(DD9Jms$ي3ӛ3m[4Z%l-qxt}{`'aIvD<}OE|qkN]Fo53Vg]Jv`A`aJNQ/]9*ݶD/."alQ(` .554`2Ebz|pej ~GB g~h_|-{O[ζPϫB&6li-m|/3׿[뿄{6t}>йcvV5 B]H͑嚔\Ii";z=+wy|߯t-q%uz+eo]ECfvX..W.cS+,%iME'DDzV L{RjNDV—|p`nQ]NoѨ!#؊(Y3 y5QV\W.w vu4l_v!mW^OEDf->]17"d.l20<K uԻ8tFř;#7.W-vA4*de,&D-Z4C7tMޚ"LLlVy$ߊ>~۫J_}ۛ;0 lAivk jDDt(OB]ǿMg66DD4#[@+ h`!F3zmik)do8+̊&9uTzh,1 ]9gc[7vcoqcC-u[*J3#}cdkGuz؄5L;zW>lSj`@fgN۞PHجv9KpJK.vc4"A h@E@*PZ wv"Q3_a~|n!̵o}wlB'{^)-@9#"h#b8Tfꕘz+wmoHgX\M9JDG(DD9NVʧn9.9+W}+/GʊOZ 5wzg?B zwsgk#gW_i]sdɗDdh=]ko?2 Vrj~a qDTQٹ~mm^qãr GW ]Κ H1=>8`K?o8ҡŅ ;'znPMS1 ]SvG?7RV/!c'߼[8/.y}fI[Wm-莕$o %f׺E6t}>йc].oJӋ_J'39o6fлljl$ue~CLNYր;X]ր.xnQS1 T̲~K+dRipp?~3u+xoj"v xMۚ]g#h`M{].:?p'.K@ptRs]cmJ:*vԅC-?q#Z.Y3 y5QVpez6䴫+$SE<5Kd.ul!րɌp}+ڤxlՄM["zW4w-d__>lqK-u6Ϝ쿽yʚi"J)qzz.QJh/n2}hhmrO}\W Uf"`:[5MԷ'jQD<%2.SS?=O)%wRfr2 I|2S4@sA%x(@s@WEEU~X_lQZXW\[/LM]$ջMCQEgC;3 ~0L:@k=Up$ǓK2Tw3=~ݎ9}!uTvoəsDd?:Sm@cֹdZ*V/wk܁p@wbz=lj[#͆dc;n63>QJO&=J+LLN?\XKÿ/#xPȧ,K ok)v552јu+F"m- \6ez}G3'3[ +S^k}z_}=Hf J7=GMK nA4x4Z,[nhXGJ 4Pn= 3t4VMӬXtsIzoM^h@d\O䷮N]P[˰G9JsPѫ] euM8ծvr]Pr!b[v]\GU <5]Du.A]L:@ϡkܮjW;z57q:{艃"ɷsծ v0շ4;KxF]Ǝg5zRk]5˥i\C].ǶEkj窭eB` r["s+7yN2Ď_8ǫ4=m[a6h1 F]=neq[DD EuMDިhێHf|"Ekr74M$cjmk|'>:9]E&3yMĪ |~H!v+jL rz dGY- IDAT|zMA h@E@*M>_:[xo `z_;ؙ_^pY_Sa.M;3Ws)z(c*EeƓ%"RH:qF#^c;"88"ciL3q;=v}pfN`kSQ"J)Qf'?I&p6_aҭV`&lB6G9j~>8[ʧTt_k{kglVq0zm%/%Tv `M XLQ0rj`3mFWWLlGš+/ - zꟽw{EkD|S`Yٙ ]5jo~\'C>fҷ%qpmQWDT`Jo}kف3WЌOv_#a{ۃ7a,{yW2CΎ利 ݗ#+5dl,ښ;f+WRhf ={hQHb#JR֕ ьP;dfǯe.pQ~0}>' b‘a`͙N\8o"}-i~xX^H]p@kd2e+tD]˩[E5޺5uCp`nQ.;N @ׄ#-d,UWo'Ѩ!#XC>x}wNl)S] }GY[D7+u-)sYի)顑okKr}^gջ(Ƕg㱬h}꒝m8?y3'oϮ@26@Me#/lq幽B6xf)^;|ޟEcь{ 72b>55ʳ3@DS<{OMJuQrvÇX*Alb2twh9`3<z*F.w)Dta3Gf<4k&v\dFVK 7*ϭ= NWhPxm WVr[d w)ŕx4t8r!Cϟu`c톗@@jxM Xm[V[hiANFs+p.K+=7 %6?=}ı#G~ɍ3`Gy8W]SW#"MM(G\|>uIw&91`o*kbhᕖK +u tVio`2ι#㝑b-/*sh~IiA7VլMOͬ,I9JN74z= MDDOr|Hifol; 46tixw_Zo=_9 Z=qzz`|]bj~PTvܕ6)LtdE!:Dӝtt`|d4i(Xy5vP{)Tv! {s~SO};vA@{%9pd:?Wv= B'!Tڥ}8Dj@%^c z/ aծ! 삭0 ${%i( `'!\ h*] ,Bwv"U_<1[` X fU`]8sgdƅ:|.!JVRlB`! tCqj@iu"{a3${@a9j@],B̥'ӎ|?Ts=94z/9}v0n{Bg"aV%JK.0P T4"U[lwWZ Py#uMM>\`_ Fwv"Q3_a~|ZnzءįۆUo@\G8sm2+m)a Tc;"8ت5-a^z{<FRǸF(G((نӺ<+_5Fg>jE4 "8o{Jq*Q~u_k{kgѴOE|5U]pD1}ۋШSTMh#j/P a"ەB B(RLCMj>i蚲 V6>Բ]M푶&Ufb7Ƨge|$tW_ffwHꟽw{]]'C>fҷ%q/ܖ=@Gg[U!@bxwΣG:wv,.J!6GMOYjl*fَnȋnౣ^&Sip1o]۲ubB4#+c \g}JJY|WSǏiL<1^o,-N< _%'ccLck[KۃSH?v9r2)Q"E"jgOktFXϾ?7;]`؋hTDjONItTtBDD\NgPOF߽>]/;:,H{&Ͻu)5'"r+f}K}P~0{rK(+cm{\ h*پu"NR̦-i ¢5Ꭰ!YD .h\TrdhIC#ɶ ԏ|$Z.D/Χ""Vo? x%OͮzEݯxDD/Ol/`Q]5 mM^4"[/ĞNnp>F+[qsقz[~wJDU̧6^s!>2 ^/z`=ilШW}c~͊O|TmGicf[Dt]e?pN(MuUf_?^ř;#3wD_=lݺ:]@jܡu;vMQԒ8JD4CYnk"j{-P2?`=R^&*Ϭs)D%"Rr[^|]5E lw`ֵ5醮[P&hT]pD4g? z5m\hZ7T1AbpEi[E?u[+}:;7, d_܍7߸Q V<{Tt-RիXғiGo>|~J g'y&6XJK. 3A h@E@*PЪ]PݯxT/h/|pXz{0_֣*mp7={PHW\MUl<Mb4#vDq?ıUkZ\  jWmݷÌ~%"DR"gنϴ nlkk+vIνxL'no}mO=Q٧lGD9st_k{kgls}e(vIνRʧTX:v|Q6=؏JNQ{rqDTQ]:UXz&@c_B(RL]H|/3׿[wv"bS%~Ꮟoi8ŗ۲Tkl+;=9481йcMeGUiky]l&vsh|p; Cl+IJ䗴i8ŗ;d7r >We7,n֖=@Gg[U!+.]m?wLi-=4t-;wFVإA;Ӌo~MӳGk5ؕ3,g%W!g~h+dR׸Y5ƭwcSC=A~x-Lg8^2lk߅ 24>i }5el|e'%1Vޭ{ʲrB{oXC3?YDDg3祟Rc6Oȧ+>Nǩمi?~#[Su!q=[*_ϟO:~7umC葟86P(D-ΐM_>D?/< =]B9=Oۑ/64`9W>Ow?I%h?1=;G17$ Oh{L]'哙 }Id[7uں;{,󞊔ySO?~˟}LpOZ|u্G~iZcE}G_~JO E䗪&7??zSVчOUր}ʾҵ EZ|j =ߓy4M?2k??V>?OϺOkowdjy}}Ɗkte;ف~U_~/VE<G~W-_,of@˙FEVoĝIJlgO9h9Kv8XO[#3w̿:t?H;\WΞ[>Pg_GԮdX*DYkc];^Ɍ~ӻu =hfe]ǡnn{}~;:R3yKw[1 _{˼ޅR>q%-yɳu-1kvz*2YGL]ZKE;+q3U.s6ilf@ʽYe^i?ΞtQPQv?Fgh}̺z+J˽嚋gOktFX4pe oud !x,+?w;򇃺d'c赑)?9T }-0q6Ѹ%֖xϜ쿽֩&4E%Էz5ZX8=4Tmmؙ ы&JZ9[D;u]vrl{^q_8{݅DDD4ӧϝ?ɋ^n)f-ڕ=nJg""J)B:3{i+b6m5躇w ɪ&)X4sw'Eq]2rYZ8[EZF@^Wt0[W"Fo.SS?N._=\u[%A\1-H%N6p쩡͏}V4;s3u*Wq[eͧE~j;ښi+DM[瓵؏&&3}'&._sP顕w[؎h+E@*xVu8+/Aqh%.)j&]ҙM-n+_nrۏkV|btࣂm;J 6nڦJ`'3}V+_PJ{-v7z^JΜ#"ՙU9Qk3~Q"^5C](Sy7L[;n;.Cv ;CovطXجx4t8r!Cez)R/EMgƍ/ҩZVR.c Uh "νM_U.s6MK/`CMnVv*}6z4Qxfi[;Y\uu7 `!Va۶γz4hCJFck\ikRO-,Fcx;%8چ=}ı#G~ShZ7T]uKa&DwF HW_S%s1̅Vݽ-*\V.̫i~W>Ks5~/x/D(fikǭ"wy^ f~ ]uVxEYnMme?YQ}^{oQ;Kp(Ň'P{/xcYyf I9JNWgyڎt*ut_07Ե+ S.z(nzM|[R\fih""e,N)s^ .""- o9JY)SPQԺn,fy[[gy sb]/IJC!,1)ox˭[_Uސ1>lj0:`[@)/t)XdV4bN LÉI&Ξ=Bmɦ''Fn񊩫O#ϴCA.Xb1j IDATԵ+#rJɛCi_Oef.p7TƮ]^<0Ovu} ͏;'iwb/o{Z]0/ޭb٩$8P<Эo}մCT59Իh;Oso_HaԍOq-f}[[gy.+|&zOlfzѤ{.b<0gʼe>YVC/>===?X }qa r|s! ՈG2׾ݱJ[ Up_ׯ髯bv}U>o_wLLT2-iCwxoITDQ\>W2 WcK:L +~ y dG5` h`3JN::0>2VK.bWo\vؔygV3 6!T4"l)T~w}SU4ur7vyoj"orK?6ý#UH^>;8]N>hGݯxT/h/|pXz{0>We7lG%lWc$\LJ5 ~OFDI~m/#vDq?ıUkZ]RI->U˧F=e`"~%"DDD96p೿;O5lE_UqG+&ٮ@c?r'+QӇu_k{kglE_U)Gr58v7/ր~Trj~ qDTQ`l0DV^0 ۲*@ؗ)F.\Ƨz"~GwLrl |H[󘆮)`eOg|$tak>uKq5t}>йcvV5 U<4{X[30uv:Z;dw+֩dX*DYk}skLo+̚}ezXsEbhX d3dnb6mI[E6]ǖ2J6zpzwv"U_4 kk|}ǯ۱3/I)YKM5MK>c2 قjhLqFes5=MNnE&c̱mjX @<ܭ@ڒ vNTLO[jri16 fꂭ^߇ضD-uV8vȱ׏>Yv1_-bx8%v'f@˙: z ɩc[[|`wIGm_FbYOYHSjjR|x w76Uhf/?8:{+DD4#g7bi k̀m[0Dr>%v'f@*FΜ8@3pisIzoM^ W *P T4*]ljuUUUUfp# g.6c4*$7? 5F&B4*4O %l1<nШ0]D9S:l7hTۨű j`@ -uvШ,ã*C5]DuvШL:@ϡkܮjW`1157q:{艃"ɷsծ Fշ4;Kx.ШƎg5zRkXi\QYmV׬U[WW&^,`CdnF5މBX[}7:6'F6JPVT5@cX`1KHoneΚ]g]dI㸮붩:qjvgg\pHG4"#6`?0<W\ѭ}8n}-{10)qi"";,h` FbCTfQEHWŢeUUEQT @L`&4ĚWrUmZ.b5vTy bxWzu̍-NC$;2fL B U=:\Yk:XK|.tcB@B@eϒiMv!lB\ά٥o/vp'Cd !Fd>k7};ݣuJ执f?wJΉ|Z/u zݣNޕZrFWOlN ďb膈!ffk9x'""兎['JU]H}um @<(aa"iiɮ)^n5n+5  bB$a`S7DLølf@OL4G Q4v h$J̈0]8WGtMD3tF ~Hpsue8D UZ(8;QujmY*?*XqjF[ծ%=zaD5խ]i7y\NjmQj疔vm*['sϛjÝ^wM=#)&"}0/^hz3RSo+ZUTqmu5y?ikɼaӦ5N%him͙]ڱh7-p_W5ED֜5,vHTunW3;tN.J/}c˦KVOlÙb( 1f%+$*#*ZXnI o;)""kKjn5{ͽK+[=7ג]Vg3~T;09n\_t}܊ۋo==j ټvUEqPo wMC3O+5-)q+f`}Ad2`TD4M?}д̂BbVw_8Q]Vt HHDD,MViol׋#Ni-hs鳈t7x 2j$6:cK`K{,}p."fЎǧ0ИQ?:2"`h6{P0"n[*?g ˡi\sITSn[uWy!wRg))86 IӍ"bDԦa(DEDTM1 c$[DD,UQܥw/̴\.CΗD2K]U=|oԷ"L+01u6¢VyG֬. 66h7;dXQV`h{OǪB`KzȀ<0D46=qƍ6o\~CO4&GYwDCw'N}/:>fPZUM8}]4gnf_Ose W{v56 iFtq-~hs]QyZS]9n// (.ZwH{]QN4&f8 hD~?Е|o}dUtNޓH_ɾ>Ie$p#e ydۖ`II˙WlH0|{D ܦ.)qvG5;.}pVwu+SpЁچKʱgv7 ܆F[fz i;Zw8Mur]i=$p 3y /u%G_/>۫|pޥ$(}~N#ދ[\u/HYyұwMWSמ{}'tAXU{^h酟68Ëv5W=_֪Vx:k㷛EU"͌~ o<-,k])I#_G/"z,Z¢۾+9 GVL3*oko5vۚC8\#"zYJR=kOl\p~̅y8`2ݴsۮ_\n߅>Z5.;?z_׏́ξ9e2_dž~^++N.\vU߾/";o%.\f?SۃٝpjUqɫVdf@'o\E5|+Wt՟1o|˗w3epew8{g.qZJe[_=W٣]e\f?yu_FEo'7VN8p˧IC,=O|UEcկyjvk=Ws".iMVߍg`1źZk=2`:w,b; E_Ż}EVo Hf@;")b-bgjǖ}fEeg _._VE7RRDҝʒ?7KInӕ?Ȯ[(Ezc1񶿷)#f| x_D/aJ/(OY//[;Yjqڟ])"Q/"gh)"[TUD$%kV-^Wa/.T~ۭӻDgL)2U{z['?7z'Z/lqō*Lf_/wς?WL1e§gLV\wF7B}4ySS\W{Ln廧z_Pũg`&0au,&"r=W\{~񊅩WL[e__ZlnZtWD{6FL~oa@2XLWz{աyRRsoHJ6kO Ibqf_+njp¥YVj]3{X;90ٔ3>Ӷrʛ9'yE3?ީy'ѕQc R ԕפ=憯yOB"?x9YF`{kp݆F[a\} EDd7?96 qw*a.1l;%Mwx﯍7b"ܢB;SI7Iy+[=G/ҙ|t 7m+(zޙ-1{5!0쑒~ț""}}2=tjex]͒)Tǒ='Npގ퍉/ÃՇvgKR7-goN-3=>)?y(}ʞzYHYM]n}C,jf B v}qgED#f@OH9 yO)3RVzkE~b-U$fK"Wܔzp~_]~zwbGhUX[ξ7G^ "emlD#ED{϶O`wo?2:w֞zj5XHˀi hGnMG(w}e]y⩪Z/u z :fJ`}W^1NV臕γm{UGehi"ay;r/O~+V/xʌZSDڤ\=]33w~Tg|$;cO`YfwgLh `_ٿ=ճƚy<~ܪODDpZI݋{DRRouђsl)hmHͳXE5Y}W?Z%q9F勺__!-UI\HWVkO׏imYpxO:|ٴ>bџ1*"0Oy L/hGna"ahn&lf8W:we^:gu岭xcԳixʁOXE)z/ӽf~$#p5fz|Zu]ׂ.eRe"mRR쟮BuE79I} ?5Rį0!"j|?:u,$g?;;+rŢ[KR}=S>rZ}^˫-fi6 b[cg W+X~Y-Q%q^3ᳰLgDR-~ߺ)%虳Ұ;Ұ{}} IDAT+,D__Vݺzk4#0E0L4E4ĝ-~b 3{,ו߽ȡ&L樔z9|/^{z_S˙9/,׮Xtf[ no,,]{нї0R],v?{ WNe{_;5ͽ7iqݦeђ=s߸O}oɶShb&_ϔʑ~(vciPʒE#뫧wleW+,YQ%q}KvXw^h8WzO2-SK֗,M*g?{Dٳв JMgDa`E7DLØy3mƯ4-{w5#xI)YгRK.^2=C?z󟇽,R4Ii3`?c3|pc-~d^uʕ+^Zl|+늯c_3ĉQ̈0f"~I> bt]NtFn-I0@c^ 7WW#HPU#?ZU߮oiZr†|n J˾[rmz$l;RLYYk>wooyб?wJ )[sKJYNjcխ]Vm@]Ա{z\Cy}eWڋ>llrCQUV%|!4-P"v_X7TӝmQ; 7֜5,vHTunW3;t^p<MqtoO\7K 55Ws[[%W[`MSװ=M>45uϷ&*V^0}i8TSp浫*ˣ;æǡVg.|Qn#waj|c&Ѡ?,ijJy4Cgnz!,‚Iwutǖ=sˎQ& hҺ}M20rTD,|j\Φe,.ql6:OP4UWn9tԪ"v"]yjoiQ6׸\nQܪo6XD,+7l,w)u/Ft0OF3uYT*Kv_iAZp =&ӝm m-\۹mO6üDg'f"Ұ[;z"`XU6CqYnh;12îl DGarP4EsfO~QX3ss-/+h`t]qU`ue=b(6KFnCQKȦahvm -v~CkviyV:.U+3cIʰdܸi7>bVpEYD7v\_E_}Bj}Ar=(\@/hZ?,#=Rjۮ_]v[c9 p "Dop+*!Ok[ b6+m7y|_$ س슈pą[-͚LCQ2W\#y`N#hEV۔+.G))pM@GޭumJ~^-RuV>,UP6p0"ClW]-e%e9`r{϶.E=m߻Е)UH8|@mEhY4I"n(^%"@3)eee߼V$lnffh>,fz䑇lץzVa[d`'ٓ] bG.]|[woQp>v*d; *Q}aǀ;q֪ wz޽>4Z8l,s4}eWKe{J$ jssw];j/H]k6n(v(zo*6Gv~FGOh ;U-؂  \D:G3J6mZT"ֆ؜9u;v,N_flkمE7=q"FCD9mX2r 2rnWcay%#kd<<ثdاVw6¢VyΟÚUҥƦ/FqׯІӧ?ͦ 70;Hߋbwz}tXKgeɦO;|AS;mT mgw."W)bZo.[޴ʻ5wuSp<{]QN4&f8 h,h$O}ݑ T t4ZצWF6n޳5ܶʛp3nx1X ;vDJV WzĈmAQDК6:W䉈D4ȶ-3&`WBu]kwL-v,qz4i=F+#F2źZ~Z5`Ӿ4]m4tdϥ>XODZ}?<6ڧ?x>^M:? ^x:'N(jIy y=F歽[WݐyD vUvw76|j=h h@B@ $4 !d$Af郛vӷ=]_Ykx+eMDR.a\a͹uZy'`&L31!"a C LvMK6Wn9n[+ [S"f@c>2 SD 1MSDLødCk֊EHþc3Z⾄mӉU>3@|3m6k[?'z@21a>S7DLø>Y0ߩp}q.N{O'$Mv|'P 9ИbFaQ3&()0 R2 DEJ-%/{<5oew\7{m-}߾ -OUdܽEz(,+z8X]uyܒ"wӮ`Xeukʼnx0ìU:$P{W}hJQ'8jFhuCWaI-.t5DMuk׈6Yk>wooyб?wJ͊xU'PHWa#=*n/!"fs_iYPZu4U V>QviK/sCy#o~Zέn/;5_XP]#B?v͹ZKŞ][oVU[_2_y鵞m }z0&IW K=ZWZ/٬1̝綾|~ŕV%XzXS[C]ޒ܆5b۵uO`TSp浫*mJxbΰq(bu1[.vHv3GOfdiz簙kKjnÃӀkT\ZQ:+썻7U? '?*tnt4޻j"N-kJjz1viTp0<"^ԃ8m"F32ev7)˳Oe@Yq^i?djyb-rhO%-.KszJizaٷnX]{u5׭ȓOS&Tt]sG,\URHӡڡ੫x)+q5ȣ_jbCX-;{n'&j;pd"Ѯd:zhhGum)‚ɝ9ҴwDpHQՉmwAu:.0 ]G]褧;۳kDDD$mv?kӳf+{̰?1MS$t(D{J,E4M~t; &d%} LI)PU貶FEwhjTk"t[t @CT]ZZUSn[k7Gڛ'9z,NSm9VG]$ ⢡`DܶLUBILs;SDq zsu+) IDATv[ce ;$5W:qcͧ6M~ҲV8l6mv;e|f~ LMwkcu@mq5_=.rC ||eQ.}Q[ryv;hzY.> Y+]DZfxF|8VUXҳ.V/M7_ קOt5Mqx+nV%c5t6¢Vy HJb-k膈aj]Z^`R݅Aiyw?putp[߻8Q=ןص3{b-5Þ%>uMȶ?SntU2T3|h uEY4͝紼:Fh`,v숔p ڂ\D^j3<f6Z 9p~ O]5uG/m{W2=Ź m|k;Zw8MuҽL@wUl;ϥpwhuCuٶ%XRRr$ *_ky]w疔儃>/So{]wa/@̖]϶sΆw[ nц mzK1fj֖ΞW^'"ħsMijfdg[Pfր0AlHf듡ȏmv3ۿ#*|И2K쵛Qʺ|%\SU]}-ݿR?vs&$/aXrFWO'V*,{gY;.ny%80!"a C LvMKPvM"յJzk[uvuf,D4L1 S0M1 cNkZqwܐ5}%D0 STu\YU1* 4#4IĦn1ӇUg|6}%D$G QӴwɪbXQ̈KW(!bF-&M%]D414MD3Tf!hKp$*PRҲo(湜v+}n-u96MUL=iZrzϹ-oW_Ǻ'ۄ.ADĒjÝ^wM=#)&2枇]3@cИk^v5T;_g3m~]ulW U[ǡDn6;{e۾v(*zl2"-K`qX)"͸L1Ee?@b{UF~ǁcg<<²}mQ}H~>'*w)jeXMH0d>jAư$ۼe8KڄaBʍ24@g.-aꮎh!1r}aqUI^31+eV;%Z zCzضdA%4_pH!"ٱ?s]ڵf<&#}c[*bphv6G@WpPh1,TOdiX;<)1*h4j+J ܯY f1Ǥ.e?xYlB$mC(@@od奝:|fɾ;N,Ť?ljר[Gs)C:}FDί>2i`߾oJU hȲL$%/;_ƌL3}V;ftc{ndzBIH. 9Xwvcb-BL;m|&ݕ\Ci}@'vh܍/VOZl$q3ZrfuScj8{Oe>Zg-53L|!1V?R濩:ݼM@clеB$mӦc{k,082::1 z\.f. }P ^j ̤25#Jq2COBh_b3=onBf&-|[q/W9uІ'h" tE""$<2E||,ML6L{~v{jaګGKWгD]l| VXխy[J3ɋ7=FIe5"B݌T.V6L{YSBrCXZrf^{ۡ]^N-Gmjm^r'X/P#obOI/^*aY^z{yf?)M+^zV@m-Kܭ[mO o$j"&#_O Nj^t$SQK;2؛fN&l/>5 FCmmr:DCG܌§= .Ws_qlco•[$z~ylz!tʇ6݆`'yDQZ=O| a˟s4hBo{FO*?o{(zlGO_`tʓSTɡ>>*A] JNiMT<,'"eebFmLNgc2%+}E]v,G^FDD?;ZrCXrUUR=u F08q楅{ycbÔˈ>nX"&dR1&D{ՍY-aZ@ hTnζz˄i+uxU8)$qcn9gLcp؉.XߺNa)S~sϘ;Fz,5TlgoZ!!mE6dgsq)`!g<@Ȃ-<ɂEjJŏ\6҅PTS#":l8D}R""k1?"UzDNnFm9jDtP4B N|ϴEiܧʏ%Z~QS7=~/mYtv^doU &*b54*zk{6;QX.Ў\bGX!W}[?>(٢nZGny^ZxhU>wvܵhָ7~W4kTe}\;Q_|O-:;[4QJs˹.$ ˁzDtBeUZ9O ;._.(v'ELԽ`+^x[a#oŹsDu'"RC011Hl4:&0Sp3g3 qY&N+zt':I(3f"6{VLt VsCXZrU/W/~E>kkS?:w-w?B|Reͳ&N&7CFƛQ q˟:&L(O{Vj)mZlé<>Lݻq[Z| 0=͂Y[ڪ[Py_)y@tEnmlNK=~GsU\e,(1vuZ ciUN%)N' S& 8홝Źyi}Zr3chTZ:k3$&yTr܅[ cG%[Y-:H-'?ʋ7 I‹Spv vI~s)JB9/>鯮m[C2M/g Ї{69jEJ17\2},syaF,v>6%X+塀t WJEѶ/羗U~Y~Z DS$3SCYʭL  OUW33[5nޒN8F%BGF0$?|Ƈ9^RX8ynyBJҵBpŌtv{e!O?0qwэÚ^Û<7n  &ַQ3O 5S'̽-WYo!%IE8(NC˼2w-9=WP+SĵLlَ;@M;D0/R;\#C㧈(,߫d""֋ N$a¨ *ݫ7 6 ާ[G2S?a=BTW]%\'JdYգLLne\~oOwUtC8??v i#^#[0W__V^;*nA~;T?*ԯQ2aLi LV"Qizn`2 C3RebI)Bg2H[nxK[.5Uv22 =O~>K sגsU 9_d)yeg .ejZzt qΟwNc Ck~ tq~֯__$}ecF|w%  7l(Ka?WOr"")NZ+3cM3Mx~!NE[Eƈq-_WDzΏ?|)O7VH#7ٽ((qHpS Łf[^|! NSbnu_w./+.m+װAc2Ѧj(1T!"G nuѕ>~{ +"`}Fgs 36ԩSԨ9vodW4ιBl:+C4;һwƋ:tg ~4I`hTtTdhoW܄>)9HvX_uʾ4\hw^lNH|wZ_VUo$A@˰RUQ]$CH{s ~K$\: _ =pA`09ή: _=' #IZu3#P@K/$:Bt! e`҂ j,i m*7pG?'1:] t1B4;|yCXX Uk͢uN3fߦvu@W TNwuAAK]@ hxYl]t"F o |L̸EzrޤRkh# 6/zn}R͂Ӫ_Ik"" $NCz&T ~RW.V #Z>NLkWKv_ X̠Gܥ>;ʳI xL= ʞUZs0GXKl-JD'xDz ct:ZYO7ղk{DmXy+dv?Pwtm V׷iZ/`?R<>N)0w ϰ$I$[Q}+^MკTf~$@>RfImKRqͭ\,äoKu?*Nڐ[V?]$!5;)p.&)cYFDnb<гD]l|fK:0Rf,o( ߑVng8g / Zoa^f젺tgpbҔD{ukޖRzeMjQ҇+uaB֐5os; ֩%螿u,Kkx媇(oYgfyRjsx8l IDAT+\N'IA 6ۉ䨨D]ditC~p۶pb7 ^z[C7zMa2؉u樧8)VBg%8 c ē01:>uZ}fD%+<Sr"RVIK.mԦ#Xlyc KL߯IgT]-W#VyyY?x.)"~nL3;{UDD͗{Bv\t3?7 lj/cia^rLXZۭ# `JTWݘRzt r:DCz?8q~" 6$ b_*q?~r6 KB9_QM%TI#3W /ɢ 8)GUnKdU5g"Ԣtl<[VH=Ə*n=/1V/NoV`J~,ȍcěEUQ">ʡd!!ZU&7k ϖJ=cDܽO.>|;?]%tb0(,l "2XbMEX =Iobww[nJD*߹՗6˜[=pQc3mZ~4rh$j?U[~glQ@J+a%l x뱵6zwM_wcA݉#"LL =Iԃ֏NtεQk<*i,7)g|:c`^F#895ͷ` b:a7\D6Dj9?T^|(]׿IH@͒A욓`%*Rݟr^|DX Ї{69ܤ ؙ\N*/"A=Wf)/QlRng:ci}ZrkZfiO'^qNw: 'ƘQsEkh29 vojS$3S]YʭL  OUW33[5nޒN8F%BGF0$?|FH*G脉Xn|XtRϕ˓n8F}-fpCTMJ[qP[ɴ*-$60I`xK-sגcR),][t>j%wp:j6g77K[0sʶn?E>Q'?'m)b#' 6bo`7 ,j0JMe«VQO2=Uh YiDV(SG1~[}_~nSPxpS.#ADD}D] Uo/W. {C|zqL5zXRo<L%Җ1e;’gE ؁>*iӥ&ʖ<~NƁB8iOTYX7w-9&]j~ݐ&ϏaG҈|O}Fgs 36ԩSԨ9vod!Cݻ/lyСƟy4 (@\^QQޛt"܄r$Q"o} /"g=/M[ߞU)b3ӆԗ?6{-;*[ku3#N6SUy֊*hED.Hr .1i%۶~LCnu3Շȱ#; Z "d"rd"YHv:&ݵ%]kfrd ;g4&/.%׮."lV@f8.pA$a4rˍ7Gqp "w' 7vMfm65y: :׈ iFWY浙ebLWqw7Xrs),@N{ͺ{kvQuޠaiɱfSe\JlbDK)Dﲗيwn.lwL1bad^zGŗLaF6+R@n\!{M]  7%;rJj~(ܖRc[wG$f@b[FgO VYQU͘͡0"#)7Pv9&L"n_\Wb`1,:%"ɶ ٿmWZJ ]I[DD,9 zC$8'H>#9ʼnwYCs-'""<$@7lQUWL i2d`tbA~YW}&scsobے0*_Ry݄vFrɎKﺘh4Z}&0,4^V6{2YaT1,TOdyE#&,rA2Q1D X‹.r;l0St9&%uO.w'xDz ct:ZYO}X H|gW8ep[> gٝ1Y@nݛwWs͇ xL= ʞM&>+tw)Tvnޏ 7_K?}Y"m|_'Pv9I,>82w˧UShk5#<ј#""Ly|S&_y1 $NCz&T ~RW.V5)O {wNwϰju}ػWgEod|Zե]y^m+5ڄ4\z=f%j%IzMD7HDz%D}Hk9Y\=9{BSHhՑ X{f>W͢bGWUb6f{7Wrw1^<kj ̤25s+?ko \m4E|{܃Tn=# 񳔓u,tOg{ xܳ՛v*Q i@{ĩEpϬo9fXL5^~2Lzk?;whk-xxo=gxKf{};Mt¸ {#ٿ|]`b߮U@C=%G=eq˼+5trD$DwM 5v%=QU Q\U."__-Nmv̸ !'>O'z΍`Fg"?DDkrı Pl5E=/|ڳr55ݿFk䑗A'=睊+"^_}L3;{UDD͗{B^ZK>Ek̼o•[$z~ylz!t .uZZh-ۭ# `JTWݘމiȱn?d<QOq"RV('>>bÔˈ>nX"&dRhs!@ Ad l|404H(/ۘQ搉BL!-tXL&)2?zKD.RMnN f=-啼y˜e\":_dM[<@Ȃ X8:Y_[$"R*F[ #.Q!eU' >Ϯq̮$D~>\1/ʋJ?">O1/-BKt5m.r3ѩe!"$ϖg"R |[R%X9 d䈒Қzs`Dp$m-Kܭ[|4{sl/pZFd /Z~gWUZdEd1T,:eFjD{"˾#pP)oe SxgkK*?^N:ޗ#J8&zw'M}bY@;v6Fb*F!iXA©''m8s~WYp"֘O9ޞ[}D ozPo?XA_yiZ򧃯i0VmtP0hklh-roR4\ʳXH2TPdsg6ʶomot;Ci ~c+,s &ᤰK2&%O*s%D[%m!Iv2rl]uh1$"D""L!Ah_Tr,/_t/3~Jl3g3}xrv\]\QN{&/1jnj鄩͞^-{<c| \l^pY&N'K?~h1DϫM;GXPk+5R7Mx~:!.(@e@)vтx)6H5]ZeIeqY. 0߻s|}kshLRt텙rr[,++WG&-.m&R-jJ1uD'WfSwʟSԧ/N{fg^#֍Sy""؄E}iϊJEbܹ*23gī՗F{>uDs9bn^LJԴwm|側^״ 9FUazpD;;?v!h*G/T%{W4kTto8ɡtED(Z[-P(v;i_9Kxo I$jJݼ0Ky4[(tYŭ8Df$:>`"hEJ17,~}iY zJDZcz-Zެ5sLŇc%uEf$^7i˚"jk^~ynBpY] ;bXlhWRY㏄A!@tR- BŌtv[A*Ifց,V&IV/y Ǩ n =ـF?Is~+p`b}۵ aBp]={nOL˵В?گZ"e۱Iظ^u-Ō(*Vͭ ?ǫT D`wSt 22\ʬ7Զ#oBC9P~+znE\N+3SMvqdy_E7cg؀v\y&qΟoSpi+m c Ck~ tq~֯__$}ekG5kb7Ʀ!T6@F-y_)1t|ʊ} m*滫(.I0^f+xEXR azX}Bz 9VH)D,yJ*g8K)ʂ\%*_@Ri=[qctbʲ†t,IKޕ~efL`)bF)oϩhk%SK>l(qml}Fgs 36ԩSԨ9vodvgԩ!Cݻw:3@h {To$A+/F"g=/M[ߞU=ؖ~Ν)@(@t"\.")o4ٶ DDd濑.Թ3%")&XL!IvlٙێTyP$ W#%%ɲLDєݵkN.@wL)0ɓSMn$cdL\K%r? $j]r`"]. "+{"[(*~.}-!HqYכ1 {@ʽOK2EZ,zWg2H$[}]Z&[Kn!d]k|QW3ZwJXYkmpIF&IRUd"2Z."CƇf% 6"cxp~^to2x7j/gg%_ pmPyE΄!iؕwfU<8a)(stLIUpa֖_`8ij'CX/5Y V/(KIdLo̳FS\Bnsi/Iӟ)*9d1e VEI;wC"B$"")BjQ軦dX Nk+2)53؍*4@[#!IF^ŽuK8F% gH޺Kq A-uWy9}d9>rL]fδ蔴({ ^0]0W޷&J0$EKN^!'3-ޖm5dkk{; Dd0'$>-V:PҴ`=;} 4g+mSQ_s:C4;һw^СC?`h /P@Bj`w*dY,.KM mܸ'Rk׮-++Pկ_'vu3t'Ntu񚯀Đ!]5_ !P@y_~]\û:zM'NLjpqmXZZZ']@(@_ ~mǜIENDB`lager-0.1.1/doc/_static/torniquete.jpg000066400000000000000000001076171451275761400177270ustar00rootroot00000000000000JFIFHHCC   $tU~-2ZP)ںy\7xrJ/п~*zMzWtfx]֒=o_5ȭnuH Ag~m\n{wSnό&d94_3ߦrqpk >=NUm(ߝyz~v_'?SV:/Xo!%ʶ5vJ/g]f`Oǥβ cx+:-۟BvjӱקbúЧrt\-ߵϣ[{~4qm{N8Gj>֍~N4xfުXGƿC-|-U$֭~N1sf&x܏4 >*3 Wlۻ'p n͜3WJYhnL}MK]cblw#u5p:ex9Pf׭6#,-+QdZ%nݺyKڵhDf1Z)UhUW W۶gҵ)+x|-nϚx>U~fRR=W굫⍚7+wn͠#ښ 'G i8rӼ:xgRGl{i^tymlvrn5Wxz2!]wN7^xힿ`vY-_ t>cS=-WkKO {Hyn {8o sFŽ#JR#0ƮJYt2|~{{ҫ1}?Z{&ߜkEZ?ZPsb=-׀*=]ZNpBJ8:.SyܲuՎcFC kގwB;7CqZJ:G .#Ҟھ|vr`EY!<8/'ns. Ŋg56u^q4&Qկtmq9c8K|zL۶7lT>zySfO\3C߄4q^gk\uq/-1t}NTXbA㭳h`K|VO9ge"}`1Q֕_rGO=]G#%V\?aPo=p`1wڻbTmOra? 8wqBZGlkMA5{%L @C>,w 9%g\HУkgYeI4f Y찇mhّ,'a{h>ڳ~¶2v*ߕ ׵+m"%df@Nrj Dgk/nֿx|'k:-2dAI61TY BgԦg4 RKS*ә?&Gz&[Q[̮aր'F荾ہۿpr^nGPu/̌>}>ZEa˥!g e*K/1-5tfmPgUCwH\ 5dDluH[-~=Q)^,b=O;mmiטiS!KB"[?Y[;5#_+g |O{߉EIp=+dhDŽ~y}ҽ[U׺"=`\뷰ǻuFŽn 1T43ת \13P+~.Bg.>axoo5=vw)ɠt 3S|?8Z^<ӽG=g8mqȂϣڀ3<0f޽ n^D8mM4:KP+ۖ^CޝMkZFKSll0Y.0jyu7C"HCt($<]MNP| aK s9c<(wMڿ)?<9<ӣ6y7?yṀi~f6VlA8lFM§r\p6Aw*t%^8,iLu,P-,IzR"dY+c5Ǜ X%xɤyX (ił?I !1AQ"aq20BRb#34r $Ccs@S?GT=`*!@v@x@g§RZd[E54`ZJɱ)@޼EO۽F[! ?މ@yrۘhO.{0*XqQ}u!DAf꩖^R?2mE~/N"n{> :Z~Pz+H1CP֦ ŤB{u.h!ƠS$61s2 J|K;{wOG|lhV k Ѫ-^@* ) t kI]nB@iSR4eb0wS(IfL+kp0K"F'rOeA)ď'i:^A tq'kk͉w_H)T}DcImeƖ-1I8/*NV+ @tXHLH4A!8 =MMlB+7ogh7ȐTӨ ,1(ʊƊk(Gd(% U%(j*gE!JJOĉ$2xRx~yLuᭊA~;zwxSua6h|Zlo 8q6=D.y{-ѭ}PpmI-XK)IJ\)u9e;lbBw,k2K'*IcyS2˷˶$y.R:˙o)xWЮ┰#AΖ4);@1RT^ 2E(PwL5AR @6&XG d4o1`gޮÄ"% He&!3V rde[ [_Q+pM;My3Ӆ%V31l: $U"9vG n13ݖESEb8e"f` 3 "Axsebb N`H1V΀IR (A~3KtP8e^vP 0eA]C[" htQ^8xJ%]d {9\ta:PGCZabRK~Lx*Dt)֔5NV$JSYrÔR(O9;>,i{BS9M5F.Q;m 0':uN"ynBa'M).Z9&0rĩ``@YDrzLdT2i5 "35 !ghd]IBL,pĔQTDI +l K ^$r)/SVr a. tNZYAP1!?D袕qS ,TTnD1A1&+T>u# :VK \E`̤7 D LZkAVE pt,dE_۵O"z/߆KU_'9Moy zr~6ak~ b; XF01,'0I&F y{Ѥ4\ѦAi0fiܐh+RIB)QL rbCeĞT]$}RaljQPQy X wϤ-1U?2"#b].zАFvI:lCՊw0ѻM7[XbL %Qv LwrTۉ8T*_xl)X9W$3 N7303z<YZ*ĐR%e`n,b0R?IR:XԎaHfy ԉK'{~9[nTѽlɤzPQc$JHq! * 'S,<^Tku9[dv+XVV 1\ү9*'ߨpdlA7"0 P iTi\%`RBĂ36ah6UzPNSYt(S`mW=" RfBJqHd'ke-2lD>Mc^VC+õx-kWY 5l,qҺzcU =e R۞ĕU+Q95Af4줬%/U~3 l >p.D(Xta 2"vR 棎-QBN3% uCTwDt)J!pc&2q'f?-<? H5ze&: uf< KCg>[L uC R$GpĦT׵2Z\й,KT9K _̣R8j,nEodNrj089Z*>O#reNx́gUtV`8&eZxE.$Kq4a;/BĿÕCˆSFEH$a rOՠl։e*䟤 Ā@9ak‚u7Cdj[ejԷ×?e_{΂izeSk}1J~@m 3j*¤u 9tFL`ގ7 DG|{K2vn{c1ײ=ދhjro3ɋ;$(O |Jkb(e!%jԒsk}bUTx&L4pH ?JldS])A|ELq.lXφv}s,hko!^a)W] D9)% M3Q!$´z!b7st5QeL@'* K~ݮ>r :#9amyV 8RDf+b.Hy8^HHÔ49LE1a59vQ᠐RiUf,ފ7KLX);8?7 ?/rR^y]R*BBJȜ6wă%KPD%bQRWD()5~ͯ uAQ!.E _] Cv .OL_7H>ɨa(`Ct91M JzdƘ[)0^V%k#Lkm;&YPHd'9O:Rp/6=(<ܪXfYmU9KG,~_ ?+WZzE U&fuڙ*#Rd7GYiiw$Q K'AY۞>ǾƹV[F li_{7( RTïЀʇv!~&7. eLO`|9L©>2[G}A"eZNәv'!O+wdIY1I~2z5G6ݷ͒e8VyS1>LiO:N J)>-7"L&H33h^կ z4s3v/U_Vo!@Sv}7NY#fns̆ }ڶP%ic?i0zNr 0|.9pa<3Yl&k x_<@ OJfۼ%f몛ʹuQ_TOˋ1VBDOwQZ{:[I98 As5'IxgoZ~զZê%'25\qy]-&fIϞ[B_H>BWlU#$hq{f*MpN},NwiȟSϝCtH,8w,ƣ{IG LyV>X}Iؿ[_{.W>WmLUx\c-5:-reidO1b7s&Nr%p4ˑCYɆ,iսYE^ eYdZNIJIiT}*˻ƃQ%,x^i O]|o y*>2{GŻ-bQ "2 G!{80H=Oaɴi3N2NvM*,|$:`:Oʊ4D4DQ`FSc8;.2o@7=:×|AE 9 ^S&g<Ј Z>ɆUgS9xNY64W 1ĥ nx-IW!fG'dA 2!HA !1AQa"q2B0Rb #r3@ScC?B*O1"90>E$́;!5Ɓם=u ;A7+ZaI !bKQ)'v^ٮe׉ jTM/B_?vq0?[9j,H c,>ASbl$ @7n'$A$md@d+rb0>B@wvUX9Y~lٽ݁-pԌjh"IMF/.XaU׾jK7T+0DKa"Ts&G d$Й]*sΖQ[^Al \#-3Rcu3#K3u`Qe xpL)s0>y}g" C>ϑ"ټz7i$&b2Ӵe5Y#ok'`x\;C1:t{Ն)EىMCRgP +*Qu 311-tHՆvLP~`|2; xOi`zWfOSޣ6B˕Q|/'t 5=Cd8%KaYRZfتky,U&~K]!0ίyp@Io,nȜWvI]0nf*oh M€kx?[[Uh, C.v$tO;i4#OTG ܂9zTUA!dQdVs~Ewazbm#n#V@kk,C`. {[THpI 6dAOWןXJW)!]aUؼADŽ\*^u. H=FŔo|&)1wx4Tfng>K劮TnJK$uGi]N|hi%6kN;R@wFGhxy>D?KTsXhHT8%Wj}"xE c$"hLNtj D;?K{4˨NjQijHxNF*Xw{ [^]d$J@]7ے&MkU(R! ]JK*T%]eU|"~ˠMv|&Lv lod䫳@|,,x_ʻz>+l+ BF_xޟ;vkRR(q(Ke #Gtz)9%tZ8c[Șn(z Hh,1t2CA_RNJ3 8P?u7̛  2jkZ?M %6KF7%^*}t=&0K @~b NF&`g3zů?D4` ˑ^/8;@ T?19X) H~ڻ*JGS4{ V_69等2ӛ=p$H%KHrճ//R8u.?0bɒW$8jaKDY]gh:T H~'#V54[o8%@WFP)[8f<f4vЃɇ;HG"8i9kA6]a.!EÇGu|]vwهGCyTpܕI~Go$#Ka)H{)A8;ȄHz"׊8R /V3uC6ͫC$"?NJ_@Hh,7'Otʧk 䁘2.Gjmɴ&g k_vZXi&W-q~x(X9vD|h, #.[7vš6z(| WX!E@TC!)7$gX7)CIHLCqʼFR8 sgنqfEy@9w-Մ2LXp:T~qwrrAsjf\!Gw -*IKŜ?}S/x_pU;;]!]bL28"bu&a"z0-kp ϶W].U16fFuʹ[W?\ ag*pJaʍa(2O)Q!cMr ꑋ'5$_v@h=|bG1lej&dShwAuه%J[Dбc(atV#B+KƤ>#lXc<43ݥphfq &6*mYU[Z)'3ɹZ3|!"EZY ORcޙbC'El{Gw=PLD|C7 dMp[~8 Ok0%cT+?ix8PCwr9 TÂPj$ %:*3nd&EbƇ{3A0\Щ."<7aK>q;|$dj@Z[Âo=E„ɜJr]MBAPM!w^bA  w b ̀Ck!b;Tϖ1Abj L?wfJIa ^$*"3 ?#ީ5ۖVjaytÇ ɭaR y}NV&|I,v}U'\Nb} `TLsxf`'1xAXث3fla cX*whUw`!z"YEd;-hu4U3,]3,K4c5"],{(^q Ig,@h1/9X .)f{`EpHMLd<3M'H*ќXr.#,+'D97)jLbXmݘgma]ٜ9̴֯SwVmLTT|4XNq1SAL6@=O[5R;31SjNuK,Cv Jq!&̾zЀX/b75$M_ +m\f](.VҩȣAǓLÝ<1FhX vƓ,ba.g'Qv  S13\k"S:YEs҄܉'j$$ZC&K 54&l-g P šfj]JDj\e&sf թ=6@e)5%38;~_CJs8fx"onVw!\M{NݎxHy6e0ŧ(M$;oh`]5``cH*lBy,Se O&e`|bU!AOL }Ե^y3|dmrF^T~@|^  !1A"Qa#02BRq $3brs4CScdtTu%6@DEUe?>Wʋqr~dSKSJorIm)[RGe^,jT(%vQy龔nKSa7Gv%&,~}fle+Z6dz$NBi݆C4٥J fQj~l 9YV9YPT6Ԧ=b;/+`buO +sA|<[+G"kQ{L2) uaKNpeÈ2u$Zf@z-k|k;5iHݷk9cn5f`ݶA$7lKP_b_趴odH߄& VU8G~w\6ӜGMtiN0}Tc %&e.lyѕ['i6R| ]HSS1^"Ն"2k cWPA79!~Fڹ.9^ܟ:=24Μڷ?~㯧/Fi" n *UuGF2)CK4 [(B)P͑d@ ^ضތnrL7frmmʫPp`'mn3ݨ)zwQ')Al Dѧ9G3jfDi)**96RKt<߆?xg|r<é!m.۱H]­Ûxۇfh́-f-K\Ghlc(|"Es#3Ab e|Hsga>10ڵR.[1gi#=zQe`L3]R2SfV{O-ƴ[8CQX +=7Q;9UʬՅGsm Z+K߽^}8X[8SYfK [/V)}c#Hx+fy3gR=R3aU[ۗ.cl6Iq!k[ X9 IBc0q1|M]!ޥRX|E!)>I8+|7r}s(nƥ(]h6Цs-lҤ-YRF)8eή0U!&W}+2DHڙM2tR017`v馇QCc _2oQh39Kl% -Io*TP/u|~:, 9'mk}lGv#AG~ ~V2;zs8%gF[M@S)m֐ũʘQtcI326%QXu%"d7A:V1@ce#;fRnY̻x8[uj mu6 ]*=2QqEGx5N]bF҂CvmQ)2 iOlwp]+l.$ Ҥq_K17~q*:5䛡—5$e@[|wl1bap~Y֒@xK{99淓m.;$v]vֵIy^4,~JU6LPι+ANJw|e#浭`; dim.;?Q:яWӎCRթJDiYCZPlsےFHC\$pL"ޭoEGH"1VWSuEԔF"fTa[uŎ%KCVl qunĠ,9;E^e;4O}vblԻyss+$"r2}V.pyVZ_-5SRpSF:Ӧ )I.v77,NNfɐa*9t[J!pX2n%@) *@NO}Ղ mH_sHHǨ2Z}L9R=3e6W[({IF=-ڵ`W)'9R҅Dy[h`XKz.hy*q){ޛDwGt.ʟKCQnz{罷2e(U%)ubNj\'ڔʝ)~gYϪӌ-׊K!fWͶ{Gt8?f/TN9lRܪkLQϵҡVtlmLcI.[9}@ YoRmvʋqS:z[}}xvXagX˨9$’uS!?1%KP vf4JYPĜEIȰ%l]leZ`T ] uof%;d!GU ̊ :TJ!7J!^SNiŶ)'ؖ2+mj`E(S\VnIupE׼& xbV,h6RS\oXT~Pu vsnpUBt}b&ls%}ըIs2?+6uH( QR˛F7ۈ1OU% ;SJ7<5N42PBۚi! R={{8aϋwZ;iUhM-[:6shPuYm6@g(h͉WPf6J!9YYFJ$,g(J}0.0ncl5j4Á:5ѝ) ,NUX$]"4xH{;跧C*ܘApeItBc&&J\kOq;P>uwbs^ZjTI-B\i,- RN$-?/F ~ʲڸ:kJt:Bz0K 2ӵ Rrf1.6TVچTU\^;+>C#V=3ô!ZSÍctO ,%)n+ W i j[gPpXPO Avmts$*V˒5uݎ8_H\װ[n!.6J) B$RT6v5KD$\є{ٔ}l Snf JuLsD8sckڄmI eHuܸ䴕Kp.{){3e i]?iKly= 3nGqtMIRɇ/e/1 Ǵ'8pl*2 rOOcga;q'J&D9`OT(͞CSoRn > yUZ~CtF IO)2):Z aj5m6:T,CDe;K6dt- D qlBG$p-2Q;TTrzOl=zI?x{(mOH=OK ԫ"A#ZIFV`ц:i!~zi~v}|岿Iׇc1z* ?;+1z=GN)8~a߫zDFgavzӎzv*)qGOc.?]qTsIFUZ''ubDiߗ~ێuRxշՋ*_C}IRzZa#XS:i'υMO91ոˎ)kl%(Hm]H0[m=rePljqWx 4'x}^ׂNpFOTzGmǶg#pP=҇=㷭/v/*tD^nHZB2Ԝ=Qө`9KL8eY&~ *72kcHlbKWٝ6;ͫҗ-YTJ]xzWRwJAJNG^lzύtc!_8P ߣCUkRຶ}L%O3Jш˷.JS%'[FH=wHUM"tXdHTh7=K i0`;vMQ$Iјѕ$XR OɇѪ}WV$ʧGC`M2}ECEV"#tVk JjS%'Q$D8̍LO#}Gyk+ePo`.ىL|IMZ )L_N# 3 D>?)AUf`T]f `K%lw>͘b %M榣0&1҆`ňCNs bv8£sX-UDpc kÞOQ56η`!s;:ėmޠGHgDZ/c22by#W8zlJԬ %Foia!JpylLO%#~v?gkjۄ+՜}1z!-ִpec$*A rzƮ"*xx:z%OqҴtTo$f]6Z;5i+p#=#>xxgdB~q|x|gEb ޿ݫN2g.ub59O\îTz_*IoA󍊜/Ki-ntMG e`_A au7@ "R}$+e)q?/on:{϶-}Rg\x3)_Kr:O^(c ,XC$ uB)5UH&n|s c}p ȣȶ4ڶ3T!3A}>3n i4(AI+92?+]@v2uzb=@NƒLd_ߞK*O \aaI":pNIB DJj 4Tsoz"s,.4\{̚۶  DN9QIw Aw*m,NNrLOpDIB7'SzRh @,$ ?FrM#'g7nQMa$Ʊ3>ՊFD mJ;UrA2J#(DL5Τ ,DfE'~ DRh4,9$lv@&$rƑk;JCB|NEƯ4hqIW$dL $k"d8Rh:7@reVܡ)qr n?j̠4C3`?}!"ߕBK5, ݄`F-H53,n(~.tE0 $E}cK<ϐE H~IkҁSKw*&fj)?X8&MYϛ`\Zbi˧ƏҀɞ%Y%LL3KQKMJNDiF;R|E>J{5 }%?:E7<Ƿ}8Jrwp`gz8-_jH Ƨ@&ce۽h MjIfxJn~JzP&֛Q%x."GbBo?} *]}1ֈ s1Й׍gwSs|ND!S N" c 7O|qPcbG:~|V?.5V$ IDh4=yi˸ LYQ΁([먻@ϥ7uA[rt>^p' ؖ%f"Q>1pPKs]dߡmY1oYߓir &Mꪜy@ !Dr&DS$O0@/l~Y|)"9*Y#,HIu 7BQ9$$c@D tY 넯kˮ|Ok鶥ώ* ,Wg>OD|O:2ݞ:LN1ͤi;.:鯋w ɒb{{G-Cj"%J0((`6L\ޓ8eB:: (f[<C^}BXu1>Sk\uFnp 7w@ %3$z=ip Uvѓ5d3 lZPк- E J}wA/dq#^SvrD86Đx;2 YdpgN#GF7\ FUYC@PW@8&NdCbL2>Y:&(V??ףhx(79]>u [Wx1WK(q}hݩ }Hu/i$Icm@1~}uLϝu~?8|yמ&jK+,b\^U6q{XG$#X3sfMTzƂvG^=5/53~|7޺lGԉfq?Ua%(ŌE'q aIP&/;}KxοͣoNr`Y'Gh'^1Q[cfB\Rۦ1JkK\]/sĜLcXrcsXV7ccd^?Z2nxg>õPko%瓽P ؽ9vg2UAe]I? `Ø056X|a X\L Nz'`ibpClLan*Ugsj=d&_DJI5xAG`^Vꡲu%%Dgn3-i,fj-hRkǎcK}DW<`LxwiDmWp篏/Wh,3Yfdyf}0FKa?+x{{mF[;+;-*PK9<r vuΦ`cIܐ> T{/9sT֙t#: qR"o9jzգ/DeUc3R8Tԫ`@ GyBSa'S0+J%F `WZTQF.N p.z$~"4ʋ+DLh|mYQo. JhЃ'VW@S;kgz=Rh`mO&*Τ`[k`XRlqa/ϯdf!T)t\) 9zMV*3ZUѺ~@.am`xRxu^J w餭!EPPW<Q"B1%!@&`gfa bpfCӟo)HЊvD5iȡ只3G\9U^}у_* Vb$LPJD5Luqg<-T4a`* y 2rc2aAE| 7"WaTO-yTZ&aiT.ݢkL_1Y>0~pQ9f ⊔Eyo*a @^P*a 0IPҰ"|,Lw ЏׅpG!7b:(ͿLJ`G]C>G`ݴЈ+,a`26EJ§~0Ts`U~qR~E X0;3׵¬PPH+=P@uVKHbuJdh(PF%FV N F*XJ@Rl>2J)ƧPʥ`AADl>=<]#E DdGm'2@Uvm13U@"Ұ#DhQ2#DJObxR*kF@n3PG$jҺh2Bֽ1O/puB>APн HUN*jGZ6Eă|Voڹ}fH$0{*!D 6ۖ;@cRJv&*B|BE)d"S"]pBٟd A4 vяObӼ˾f^DboEBcA3ӆ%AyL U66e B&!rIRL tBhv(qc 0$ Q̐eZPs2 pE$(T,h뛡??;-csC)}h cRSʖ5x:#c)(,|u,0q3U DhSlY(  F!C(Hh ` 2J ^_$)EK 1ExY]@T ,{8>2M7 %]Ć]KB{Ū\)"!?@!JJi ~諢Ċ?nIQ"> RTViTt+"Z\m@#VB? a$*cT5VRF3[ѪCv1D(`@*cG1h" <@Ъ#( "'Vf)UChl#֓f8`]Jd-L AؿI 0H4È#S~^#B.Z`Z)R F b麰` 0HAFƫ1@d~N,L=~&H}6=O[kă1AxP"a"#mYh(YV" WRcP ‰t )ѸzSS&QJ˷M# wۆ&`+ P,hQ^q~H1OD(m B 8Atad5ߦ/I* *v'#*(x S|<\ Dx(DQrDUD O脳!hLqKGFXad> c0?q3Pl{0 |{wX):}- T4`,=4`ps&<N]PR}Xؕ4FZ- 9P{"<4j'1TA2(DI>-ÅPemR}S"iS)L-O2Pc.6̫=rX!(9#!oOrA]QP:tVC78+AUҐ$[V9+ 8 S힨!B~Jp TRэ" H5 A<_pj`mP 6a@?w pQa,Ԧ*+9BF?<1hbj`_Z!:C\9*?(ԋcpX'a7L`>(ƐAtQS+G Z9RjpIm+˥ΩT"iK(%R"X8 m"4y.h?DT,?4=TxpqPSK.EO\ p|U,J<ѠJ_AsD%$Dp|U*Cŝ:jx+2GPz H]7 8 Eu"* IZ(xչ]Th-dC~VGS&Ap*rlTǐ#gi(4,T(81$YB 4 xBv{5ʠqWSh^ޚv-^'3N)#r?U*U@?c>TA8 aMK"A붒衅6JQi[ƨbR#h8WE޶ ޔA _F[Yȡhp*oV0 QT21 Ah'DD]|bˌtRPW Q^.)$F``&ŤAP#(4Wl 0m\K1@LxqGF?jN]Q(DhHDuiA%B9UAtV;ˀ΁FQWVSb/OdA(C!Uu7&0 j~_S~u {SCuuЄ _lHqjP9 42C% ~!* )@b66-R"XBZΊE}:L 0gPR?B*`2u3ɍi#+D4KE@BVYo3 %i QU .$lhaxDoeRXhh@Hрs@ Q^SfѪAQBihChbԃB hF4hKgDi& wPı054 t@hj$+"BBPKA`؂Px( ~Ǣ2"8UYTʋ:yIEV!L*XG}#}Z2i~`8%\=S.*" k7sWض R-7V!J 5b9Q:E WEM!X SM!5WF .E=ħ $X0A%4*Q8A4?o9 `qH)N0w2DP  [HU4jCН u% uY 0W5Q>!aCUt(õYk@h*,a D j#QhAHJP!էz $@<t|,UD@^"2QA=42UJ5= ?o|eG-1 bJN! SQ -Fuo"2* QR 58/Ezgf* &kH/"ZHFt E1iARɡd(B #WF*8ƣXdGTpTeU#,HR=X"`L -Jb>A $!?F*S'?BuZ%G\B$B86|p#VJ#PhY1VCeu˰C1K+n?̬45JRT[+x|:'׃uVs%asY tAŢ@ h/'*5ҖpL*/ Zԗӯo$TN(ccA4#~ 1IP` $p2)FM璕r`jGC*7aojBQ+@Bn[ѥ)dA!H $d0Xb[Lp|P "jaR:W^ taC_ eTWp .5DqqI8PXGQ )z"GU~R>g]jА7CV%RSM! I*Kp4` *QB6/314x{ tTDt#C е<K@ `j$# h7 AIY JEAB/1sxb9OҪூTd'G[XXKU4JzTOE~$yB?ֆ%7IQ *2 @ĒzON2$ O IXL )f1f(܊!%8P/Qh 4>:+wR!PLd jpTtw6:! *%A2pA062 '0!+2u9B*Y_5*X zj½S RK#rѡ7  4^",LaTD:p E\JO(Ȁ`H+fQwOecX nBi@R6}F()K(5S^}(4 a)}Dj@2z&}dP nh`ĠN_#@BEI*8iK"8["F@8ڕ @S m+BX@QG%(6:,tA2o$РM!E?( P9\~S#~T^'4kiHP`Pf*24Ll׎p!g= @R"R|"h06g*5yEr(E!~ IS [E,%"Ŧ([[5J*S'J `mC P[lR*I!2m_iqbMT!{!~#%l1?X %* +4TЀKQ ?"BYs bx+5%2Ɏ"[bU#ݬCB]k[P._@J M( RSE Rթ=H@_U)@xḮPAat-!_tKը`y f0S ]2P$?!WEWPTX0:!@EU7 *6Ҍ8P߼k"!1AQ0aq @?Wև(&BP)X@&FeS.h"ި> ;H(Pzm t﷤ȏ / ?5xVC$MJ&/5:rDD|U^|0t5F) 6co͒ 䑍<;_Y2$M&xUoodKutmW\ ޣgUU0zrzS~M%0"_ovd:ult|bkC CN{BefֲUߒ:=xU _OUGR Xy@*ioζ_W@RjcI'V$_ ǒ[RC!a` -ؘ+c*} C  !@=8W9ةU<0f5I B9+DFmcK^j4F-'-*AvG!qɽ.,*b‒p,Eb0f[RSpqCX@\bQd@J0v EtT>k2D%kC` 3E 0Of*D8) nPR/|)!/m ȊP ZsC-N)^@-$ ۢybӤ[<Vғ$ R[e:1/ ,# **=6?RPj+*iM|A:CX!б4U.EgQ8`#`O! B; J! BCe Rml-QVח( OKT Jī5(oDU ʁ܁ *4+L+* AX b MX `P脃JCS( ֠D}luc)!V 'Lq)E((#Tm [x NR&:-kGAWE숀%w46(BΣ!9 @([DCsnɮ0ւyk<>Nm:;6ՙ@(lȡrj]<9Q*@սUBhC ;{ JT_mZ &:,Q6蜬j#xr编; jr}T}9D. :Mfx|+RyM U!`1i<ׇ5(> `:(X}`.~$|,n+`q Dy-pRI; O:d H!&( Tq$Elz E̲6`| !k Њ5tf0L\/"]#x-:6Gq!cQH)i|RȞK$(| ${/X訊)7qÑ@֠Вx<ƞ=^,9D"Cjg&3j!;=!czwT`Z/Ņ4̺eo9s¿}+p pW AT*HQ㎀J`5PDR$4`EIiIi7OԨGEr+0C l "A*ነ/pe7x|51:i3k#Y ;(&EH;H@k;E#ȸپ~2 1~WL`A(8 Ua y9$Oـ `܈Y @,CwUbWFu%@Jـs cNTß^G-Q,m0N YtjF` `-PS9PF*[l,ǣ@h035@0^ BNq|1]GzPX0ة qS@Lb"g0TtR}WB]&˨&e)5opP.\(%̯\$?LzSyg2Zh?0}m kP Qg\5xө< yP8 pHrEI|GG [=ϙ~6i|1a +ӠsTLԠ)K TZ |m 1 JAWGM"%( oS欇V{,0`y2 .)~T?z2 DDˉ)0ۀxsy*8?0RIh>佞j_m~BAOPHUGZo&2Q@:)qJkJ"3=pTԩ_@/ȭBU!UK); _r6¡wsL+?)ZHF:yErDR:@U_ALBVt# f&V _ M=H9ԘW b i H OҔWj$_tD'#ח&_(g?_Bi6*G h Tx-iM`=!kЅ@ (@vV/C4@ZaRNcR@;cT(e =;sȕd0< wz~KJPo $QR aBJO?71C*ѽ25B GtQ>+"D*~Rhh!g%9 ;M *  q,,YĵgT$1K_X&(/jh~.il \*敪4'RNk@C3@ -/H*7[O˪S+nT@ S壊y9 ݴPL[ R䡖 ku-¾P@h! ;n?.h!e >&El# T]żR=Su3ӆZ%VזytLb2 @|-6xSM+C&>-*VRPP+HUH [Nޔ+`*3\~\D)Ay)HGDV6BH* PhH`QRN\[i`"v`TU[m> @@jQ4HɄb6gAb#DĹg ]En.qCDb(BApêH Q$d ѥ@l0oB #F ¢@ZK 1٦*. "F\")( 郣Oב\ BFnH:޻ _P GIS< Z@Cj] U$&#Zu-?h28x uBS:*M1@ۃο4T/2Ǩ؄2u Q`1b!)YĴ8<5cC}"?tDQth^CU>,y `,X,*shIaN !-" Jσ MBJDXq)4M@>XRE()Z`*>dpO3$PKzEXòI/6 ap!"+U#P@T!\ж.Tu-׀QIViR K(}ulU"@)`HԎ[xTgN@BIsa|О0X]X(D*6F3.TCVO D*&z 7Ve)P$Q ZQZb(tc}̼)䶵C B HbZ[54"%'ֺ ~)$qbSZ RFYت}_OH"]#!} 8 image/svg+xml Locked Un- locked Coin Coin Push Push lager-0.1.1/doc/actions.rst000066400000000000000000000176571451275761400155760ustar00rootroot00000000000000 .. _action: .. _actions: Actions ======= Actions are values that determine external events and user interactions that cause the :ref:`model` to change, as expressed in `business logic`_ language. .. _business logic: https://en.wikipedia.org/wiki/Business_logic Actions are values ------------------ It is important to note that **actions are values**. They are a declarative description of what may happen, not the happening in itself. In the :ref:`Model` section we discuss value-semantic design in detail. All those concerns also apply to the design of actions. Type-safe actions ----------------- There are many ways you can define actions. Normally, in an application there are different *kinds* of actions. Consider a typical CRUD_ application, like the canonical `Todo List`_ example. To let the type system help us deal with the different actions, we may define the actions as different types whose instances carry all the information needed to perform the operation: .. _crud: https://en.wikipedia.org/wiki/Create%2C_read%2C_update_and_delete .. _todo list: http://todomvc.com/examples/elm/ .. _todo-actions: .. code-block:: c++ struct add_todo { std::string content; }; struct remove_todo { std::size_t index; }; struct toggle_todo { std::size_t index; }; These actions may act on a :ref:`model` like this: .. code-block:: c++ struct todo { immer::box content; bool completed = false; }; struct todos_model { immer::flex_vector todos; }; .. note:: We use here the containers types from the Immer_ library of immutable data-structures instead of those of the standard library. These are discussed in the :ref:`performance ` section. .. _immer: https://github.com/arximboldi/immer We can now implement a :ref:`reducer` for each of the operations as overloads of an ``update_todos()`` function: .. code-block:: c++ todos_model update_todos(todos_model m, add_todo a) { m.todos = std::move(m.todos).push_back({a.content, false}); return m; } todos_model update_todos(todos_model m, remove_todo a) { m.todos = std::move(m.todos).erase(a.index); return m; } todos_model update_todos(todos_model m, toggle_todo a) { m.todos = std::move(m.todos).update(a.index, [] (auto t) { t.completed = !t.completed; return t; }); return m; } Once we have this family of actions and their corresponding reducers, we can use `std::variant`_ and `std::visit`_ to combine them into one single type and function, that we can use when building the :cpp:class:`lager::store`: .. _std::variant: https://en.cppreference.com/w/cpp/utility/variant .. _std::visit: https://en.cppreference.com/w/cpp/utility/variant/visit .. code-block:: c++ using todo_action = std::variant< add_action, remove_action, toggle_action >; todos_model update(todos_model m, todos_action a) { return std::visit([&] (auto a) { return update_todos(m, a); }, a); } This approach of using ``std::variant`` to combine strongly typed actions has multiple advantages: - Actions are simple :ref:`value types `. It is easy to add serialization and other inspection mechanisms. - We can use `function overloading`_ to distinguish different types of actions. - When `pattern matching`_ the combined action type the compiler will complain if we fail to cover some cases. - It works well when composing components hierarchically. We will discuss this in the :ref:`modularity` section. .. _pattern matching: https://en.wikipedia.org/wiki/Pattern_matching .. _function overloading: http://www.cplusplus.com/doc/tutorial/functions2/ .. tip:: You do not need to write one separate *reducer* function per action type, like we did in this section. In the :ref:`architecture` section we showed how to use :cpp:class:`lager::visitor` to :ref:`pattern match the action variant using lambdas`. This lowers the amount of boiler-plate required for small reducers. There are other libraries like Scelta_, Atria_ or `Boost.Hof`_ that are convenient when dealing with variants. .. _scelta: https://github.com/SuperV1234/scelta .. _atria: https://github.com/Ableton/atria .. _Boost.Hof: https://www.boost.org/doc/libs/release/libs/hof/doc/html/doc/index.html Alternative schemes ------------------- While type-safe action is the preferred way of defining actions, and the one used most often in this document, it is important to note that you can freely define actions however you want, and there are situations where other alternative designs might be better. Stringly typed actions ~~~~~~~~~~~~~~~~~~~~~~ Instead of using types and variants, you could use ``enum`` and ``switch``/``case`` to identify the different kinds of actions. You still need to somehow access the different kinds of arguments to the actions, for which you may need to resort to ``union`` or mechanism, which is unsafe while bringing no additional advantages. In Redux_, because of JavaScript, they often use instead `stringly typed`_ actions. This is rarely advantageous in C++, but there are situations where you may want to do so, for instance, when implementing a command line or configurable shortcuts. When doing so, it is still useful to have a type safe core set of actions, and to implement the stringly typed ones in terms of them. For example, we can extend the :ref:`todo actions` defined above by adding a string-based action type and a corresponding reducer: .. _redux: https://redux.js.org/basics/actions .. _stringly typed: http://wiki.c2.com/?StringlyTyped .. _intent-example: .. code-block:: c++ struct todos_command { std::string command; std::string argument; }; todos_model update(todos_model m, todos_command c) { static const auto command_actions = std::map>{ "add", [] (auto arg) { return add_todo{arg}; }, "remove", [] (auto arg) { return remove_todo{std::stoi(arg)}; }, "toggle", [] (auto arg) { return toggle_todo{std::stoi(arg)}; }, }; auto it = command_actions.find(c.command); if (it == command_actions.end()) return m; else return update(m, it->second(c.argument)); } This can also be considered an alternative way of implementing an ``intent()`` function, as :ref:`suggested in the Architecture section`. Function actions ~~~~~~~~~~~~~~~~ Some people consider that separating action types and reducers is a form of boiler plate. As such, they are tempted to combine the two. For example, the :ref:`todos actions` and reducer could be rewriten as: .. code-block:: c++ using todos_action = std::function; todos_action add_todo(std::string content) { return [=] (auto m) { m.todos = std::move(m.todos).push_back({a.content, false}); return m; }; }; todos_action remove_todo(std::size_t index) { return [=] (auto m) { m.todos = std::move(m.todos).erase(index); return m; }; } todos_model toggle_todo(std::size_t index) { return [=] (auto m) { m.todos = std::move(m.todos).update(a.index, [] (auto t) { t.completed = !t.completed; return t; }); return m; }; } todos_model update(todos_model model, todos_action action) { return action(model); } This approach is, in general, not recommended. While functions that do not capture references are, in fact, values, they are so only in a rather weak sense. They are opaque, imposing several limitations: - We can not properly define equality of functions. - The arguments of the action, once captured, can not be inspected. - They can not be serialized. lager-0.1.1/doc/architecture.rst000066400000000000000000000314571451275761400166120ustar00rootroot00000000000000.. _architecture: Architecture ============ This section gives an overview of the architectural elements of a Lager based application. It offers an introductory tutorial with plenty of pointers into other sections that cover the various topics in more detail. Unidirectional-data flow ------------------------ .. image:: _static/architecture.svg :width: 90% :align: center This diagram represents the architecture of a Lager based application, the so-called *unidirectional data flow architecture*. It is somewhat similar to common representations of `Model View Controller`_, however, there are some fundamental differences: * The *boxes* do not represent stateful objects with their own identity, but **value-types** with deep-copy and logical equality semantics. * The *arrows* do not represent references or aggregation, but **pure functions** that only transform data. There are only two kinds of stateful procedures, the decoupled :ref:`effects `, and the :cpp:func:`dispatch() ` method. We will talk about them later. In the following sections, we will focus on the values types and pure functions that sit at the core of a typical Lager application. .. _model view controller: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller .. note:: The diagram above presents the unidirectional data-flow as supported by Lager, but there are proposals. André Staltz gives `a thorough overview`_ on the subtle differences between various instances of the unidirectional data-flow architecture. .. _a thorough overview: https://staltz.com/unidirectional-user-interface-architectures.html Basic example ------------- Let's build a very basic example. For now, it is going to be an interactive application where you can use commands to manipulate a counter. We follow a `value-oriented design`_. we will start by looking at the data we need, in the form of value types. Then we will look at the transformations required. .. _data-oriented-design: .. admonition:: Differences with Data-Oriented Design. Over the last years, we have seen `data-oriented design`_ as a new trend in the C++ programming community aiming to go beyond the object oriented programming paradigm. Like DOD, *value-oriented design* breaks encapsulation in order to separate data and transformations, such that they can be studied explicitly and independently. Often, we end up reaching similar conclusions: we decouple entities from their representation, we treat :ref:`identity ` explicitly, and prefer :ref:`normalized ` data. However DOD is very focused on game development. As such it focuses on performance above all, puting special focus on cache locality and mutation. Maintanibility is not that important is games (at least outside of the engine code) since once a game is released, the next one is started on a blank slate. In contrast, some of us in the VOD camp come from the development of professional desktop software. While performance is also important in this domain, these are decades-old codebases, where intricate document models need persistence with backwards compatiblity, and implement complicated workflows that need to reliably support undo and feel responsive. As such, we focus more on local reasoning, reuse, maintainability and concurrency. .. _data-oriented design: http://www.dataorienteddesign.com/dodbook/ .. _value-oriented design: https://www.youtube.com/watch?v=_oBx_NbLghY Model ~~~~~ The :ref:`model ` is a value type that contains a snapshot of the state of the application. For our example application, something like this suffices. .. code-block:: c++ struct model { int value = 0; }; Actually, we could just have aliased the ``int``, but normally we will have more complicated models with multiple pieces of data bound together in a ``struct``. Actions ~~~~~~~ The :ref:`action ` is a value type that represents something that the user did in the application. Normally, the user can do multiple things. We can use different *structs* to represent the various things that the user can do, then group them together using a `std::variant`_ .. code-block:: c++ struct increment_action {}; struct decrement_action {}; struct reset_action { int new_value = 0; }; using action = std::variant; .. _std::variant: https://en.cppreference.com/w/cpp/utility/variant .. _business logic: https://en.wikipedia.org/wiki/Business_logic Reducer ~~~~~~~ Now that we have defined our data, we need to define the update logic, the :ref:`reducer `. This a `pure function`_ that takes the current state of the world (the model), some external stimuli (an action), and returns the updated version of the world. This would look like this: .. _pure function: https://en.wikipedia.org/wiki/Pure_function .. _pattern-match-example: .. code-block:: c++ #include model update(model c, action action) { return std::visit(lager::visitor{ [&](increment_action) { ++ c.value; return c; }, [&](decrement_action) { -- c.value; return c; }, [&](reset_action a) { c.value = a.new_value; return c; }, }, action); } Notice that we take and return everything **by value**. This makes the function *pure*, even though we do use mutation inside the function---these mutations are contrained to local variables. This somewhat reconciles value-oriented design and procedural programming. We focus on the purity of our function interface, but in the implementation, you may choose a more functional or procedural style at your convenience in an idiomatic C++ style. Also, don't be intimidated by that :cpp:class:`lager::visitor`, you can just mentally parse it as a ``switch``/``case`` on `std::variant`_. Wrapping things up ------------------ We have defined the model of our application. Maybe this is a bit overkill for such a small program, but notice the architectural properties of such design: * The model is completely independent of the UI, we have a reusable definition of data and logic that we can use to make different user interfaces. * The core logic is very easy to test. Purity makes this specially true, since updates are not destructive we can compare the inputs with the outputs (the past and the present). This is how a test could look like using the Catch_ framework: .. code-block:: c++ TEST_CASE("counter increment") { const auto old_model = model{0}; const auto new_model = update(old_value, increment_action{}); CHECK(new_model.value == old_model.value + 1); } This ability to make explicit assertions about change also makes advanced testing technices like `property based testing`_ possible. * We can now generically implement :ref:`undo ` and :ref:`time travel`. .. _catch: https://github.com/catchorg/Catch2 .. _property based testing: https://en.wikipedia.org/wiki/QuickCheck User interface -------------- We will now provide a very simple command line user interface for our counter application. We can type in the commands **+** (plus sign), **-** (minus sign) and **.** (dot) to *increment*, *decrement* and *reset to zero* the counter respectively. Each time the counter changes, its current value is printed to the terminal. A session could look like this:: current value: 0 +++ current value: 1 current value: 2 current value: 3 - current value: 2 . current value: 0 .. _intent: Intent ~~~~~~ The user interacts with the application by typing characters. Each of these characters represents one command. In a way, each character can be considered *action*. However these actions are specific to the user interface, we need to transform them to the actions corresponding to model operations. There are several ways to do this. Later, we will learn how to :ref:`use effects to dispatch actions as a response to other actions`. However, in our current scenario, a function suffices. We call this function ``intent()``, because it captures the intention of the user, this is, given something that happened in the user interface, it tells us what the user wants to do: .. code-block:: c++ std::optional intent(char event) { switch (event) { case '+': return increment_action{}; case '-': return decrement_action{}; case '.': return reset_action{}; default: return std::nullopt; } } Draw ~~~~ Finally, we need to present the current state to the user. This is the function that renders the user interface. It is a function that only reads the model, and transforms it into a view. This *view* could be represented as a value itself---this is the case in Elm_ or if we use Redux_ in combination with React_. .. _elm: https://elm-lang.org .. _redux: https://redux.js.org .. _react: https://reactjs.org However, if we are using an `immediate mode`_ interface like ImGui_ or ncurses_, we can consider the view a procedure that reads the model and outputs a frame of the UI as a side-effect. This suffices for our purposes now: .. _immediate mode: https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics) .. _imgui: https://github.com/ocornut/imgui .. _ncurses: https://en.wikipedia.org/wiki/Ncurses .. code-block:: c++ void draw(counter::model curr) { std::cout << "current value: " << curr.value << '\n'; } .. tip:: In previous versions of the library, the view function used be provided the previous value of the model too. This allowed the view to decide which parts of the view do need to be updated based on the differences between the old and new model values. If this behavior is wanted, it can be simulated by wrapping the view function in a lambda like this. .. code-block:: c++ [prev = store.get()] (auto curr) mutable { draw(prev, curr); prev = curr; } Glueing things together ----------------------- Now we have all the parts that build an application, we can finally put things together. Event loop ~~~~~~~~~~ An interactive application is composed of a main loop. In many UI frameworks, this is under control of the library, following the `Hollywood principle`_. Lager provides hooks such that one can teach the :ref:`store ` how to interact with the :ref:`event loop `. In this case, we will write the main loop ourselves, so we can just use :cpp:class:`lager::with_manual_event_loop`. .. _hollywood principle: http://wiki.c2.com/?HollywoodPrinciple The store ~~~~~~~~~ The main component provided by the library is the :cpp:class:`lager::store`. You make one by providing an action type, the initial model state, the reducer, the event loop interface. The *store* will then provide a thread-safe :cpp:func:`dispatch() ` method that can be used to inject actions in the system. Whenever it receives an action, it will evaluate the *reducer* in the event-loop to update the state, and trigger a redraw. Main loop ~~~~~~~~~ With these components, we can finally implement the ``main()`` procedure of our application: .. code-block:: c++ #include #include int main() { auto store = lager::make_store( model{}, lager::with_manual_event_loop{}); watch(store, draw); auto event = char{}; while (std::cin >> event) { if (auto act = intent(event)) store.dispatch(*act); } } Next steps ---------- In this section we have learnt the basics on how to design an application using Lager and the *unidirection data-flow architecture*. Now you can read further in this user guide: * In the :ref:`model`, :ref:`actions` and :ref:`reducers` section you can learn more about how to design the core of your application. * In the :ref:`effects` section you will learn how to perform side-effects (loading and writing files, talking to a data-base, dispatching new actions) without making the *reducer* impure, and how to do :ref:`dependency injection ` to pass around the associated services. * In the :ref:`modularity` section you will learn how to make complex applications out of smaller components, both concrete and :ref:`generic `. * In the :ref:`views` section you can learn how to integrate different UI technologies with your Lager application. * The :ref:`time travel ` section shows how you can use Lager for time travelling debugging and introspection. lager-0.1.1/doc/conf.py000066400000000000000000000306461451275761400146740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # lager documentation build configuration file, created by # sphinx-quickstart on Thu Oct 27 18:10:24 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.mathjax', 'breathe', ] breathe_projects = { "lager": "_doxygen/xml" } breathe_default_project = "lager" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # from recommonmark.parser import CommonMarkParser from recommonmark.transform import AutoStructify source_parsers = { '.md': CommonMarkParser, } source_suffix = ['.rst', '.md'] def setup(app): app.add_config_value('recommonmark_config', { 'enable_eval_rst': True, }, True) app.add_transform(AutoStructify) # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'lager' copyright = u'2016, 2017 Juan Pedro Bolivar Puente' author = u'Juan Pedro Bolivar Puente' raw_enabled = True # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'0.0.0' # The full version, including alpha/beta/rc tags. release = u'0.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # import sys import os.path sys.path.append(os.path.join(os.path.dirname(__file__), '../tools/sinusoidal-sphinx-theme')) import sinusoidal_sphinx_theme html_theme_path = sinusoidal_sphinx_theme.html_theme_path() html_theme = 'sinusoidal_sphinx_theme' extensions.append("sinusoidal_sphinx_theme") html_theme_options = { "project_nav_name": "lager", "github_link" : "https://github.com/arximboldi/lager", } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = u'lager v0.0.0' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = '_static/logo-black.svg' # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'lagerdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'lager.tex', u'lager Documentation', u'Juan Pedro Bolivar Puente', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'lager', u'lager Documentation', [author], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'lager', u'lager Documentation', author, 'lager', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The basename for the epub file. It defaults to the project name. # epub_basename = project # The HTML theme for the epub output. Since the default themes are not # optimized for small screen space, using the same theme for HTML and epub # output is usually not wise. This defaults to 'epub', a theme designed to save # visual space. # # epub_theme = 'epub' # The language of the text. It defaults to the language option # or 'en' if the language is not set. # # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # # epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. # # epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. # # epub_tocdepth = 3 # Allow duplicate toc entries. # # epub_tocdup = True # Choose between 'default' and 'includehidden'. # # epub_tocscope = 'default' # Fix unsupported image types using the Pillow. # # epub_fix_images = False # Scale large images. # # epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. # # epub_show_urls = 'inline' # If false, no index is generated. # # epub_use_index = True lager-0.1.1/doc/context.rst000066400000000000000000000003271451275761400156040ustar00rootroot00000000000000 context ======= context ------- .. doxygenstruct:: lager::actions .. doxygenstruct:: lager::context :members: :undoc-members: .. _effect-type: effect ------ .. doxygengroup:: effects :content-only: lager-0.1.1/doc/cursor.rst000066400000000000000000000001171451275761400154320ustar00rootroot00000000000000 cursor ====== .. doxygengroup:: cursors :project: lager :content-only: lager-0.1.1/doc/cursors.rst000066400000000000000000000404661451275761400156300ustar00rootroot00000000000000.. _cursors: Cursors ======= Built upon `transducers`_ and :ref:`lenses`, cursors are a great way to bridge value-oriented designs and object-oriented systems. It enables modularity by allowing one to write components that reference mutable and observable data abstracting away the shape of the data storage. .. _transducers: https://github.com/arximboldi/zug .. _types-of-cursors: Types of cursors ---------------- Cursors are wrappers for :ref:`models ` where you can get the current version of a model, watch it for changes, transform into other cursors and (for some types of cursors) replace the model with another version: There are many types of cursors in Lager. Based on their interfaces, one can divide cursors into read-only cursors, write-only cursors, and read-write cursors. Based on their source of information, one can divide cursors into root cursors which directly holds the model, and derived cursors which does not directly hold the model. Interfaces ~~~~~~~~~~ * ``lager::reader`` is the read-only cursor interface in lager. It provides ``get()`` and ``watch()`` functionalities: .. code-block:: c++ #include // pseudo-code for the call signatures model_type lager::reader::get(); void lager::watch(lager::reader cursor, std::function callback); ``get()`` will return the value of the model in the cursor. ``watch()`` will make the cursor call ``callback`` every time the content in it has changed (checked using ``operator==()``). * ``lager::writer`` is the write-only cursor interface in lager. It provides the ``set()`` and ``update()`` functionalities: .. code-block:: c++ #include // pseudo-code for the call signatures void lager::writer::set(model_type new_model); void lager::writer::update( std::function callback); ``set()`` will replace the value of the model in the cursor. ``update()`` will call ``callback`` with the current value of the model in the cursor, and replace the value with what ``callback`` returns. * ``lager::cursor`` is the read-write cursor interface in lager. It inherits from ``lager::reader`` and ``lager::writer``, and has the functionalities of both. The ``lager::cursor`` class is can be made available by: .. code-block:: c++ #include Root cursors ~~~~~~~~~~~~ There are four root cursors in lager. All these cursors can serve as the "single source of truth" for other cursors. * ``lager::state`` is a subclass of ``lager::cursor``. One can create a ``lager::state`` by: .. code-block:: c++ #include auto state = lager::make_state(model_value); // or auto state = lager::make_state(model_value, tag_value); ``tag_value`` is a instance of one of ``lager::transactional_tag`` and ``lager::automatic_tag``. If it is not provided, ``transactional_tag`` is used. The difference between these two are when the value in the state gets propagated (i.e. becomes accessible via ``get()``). ``transactional_tag`` requires a ``lager::commit()`` call before the value gets propagated, while ``automatic_tag`` does not: .. code-block:: c++ using model = int; auto state = lager::make_state(model{}); std::cout << state.get() << std::endl; // 0 state.set(1); std::cout << state.get() << std::endl; // 0 lager::commit(state); std::cout << state.get() << std::endl; // 1 auto state2 = lager::make_state(model{}, lager::automatic_tag{}); state2.set(2); std::cout << state2.get() << std::endl; // 2 * ``lager::store`` is a subclass of ``lager::reader``. It makes changes to models by dispatching :ref:`actions`, instead of the ``set()`` function. One can create a ``lager::store`` by the following code. For more information, see :ref:`store`. .. code-block:: c++ #include auto store = lager::make_store( model, event_loop, enhancers...); * ``lager::sensor`` is a subclass of ``lager::reader``. It takes a function and use its result as the value of the underlying model. .. code-block:: c++ #include int foo = 5; auto func = [&] { return foo; }; auto sensor = lager::make_sensor(func); One can make the sensor re-evaluate the function and update the value inside it. The re-evaluation only happens when ``lager::commit()`` is called on the sensor. .. code-block:: c++ #include std::cout << sensor.get() << std::endl; // 5 foo = 8; std::cout << sensor.get() << std::endl; // 5 lager::commit(sensor); std::cout << sensor.get() << std::endl; // 8 * ``lager::constant`` is a subclass of ``lager::reader``. It takes a value and use it as the value of the underlying model. The value cannot be changed later. .. code-block:: c++ #include int foo = 5; auto constant = lager::make_constant(5); // Always prints 5, as long as `constant` is not re-assigned std::cout << constant.get() << std::endl; Derived cursors ~~~~~~~~~~~~~~~ Derived cursors are all cursors that are not root cursors. They are obtained by transforming other cursors using the methods described below. .. _zooming-with-lenses: Zooming with lenses ------------------- One can use ``zoom()`` method to zoom a cursor into another: .. code-block:: c++ auto cursor_type::zoom(lens) -> maybe_other_cursor_type; For example: .. code-block:: c++ #include using map_t = immer::map; using arr_t = immer::array; struct whole { part p; map_t m; arr_t a; }; lager::state state = lager::make_state(whole{}); lager::cursor part_cursor = state.zoom(lager::lenses::attr(&whole::p)); lager::cursor map_cursor = state.zoom(lager::lenses::attr(&whole::m)); lager::cursor int_cursor = map_cursor.zoom(lager::lenses::at("foo")) .zoom(lager::lenses::or_default); lager::cursor str_cursor = state.zoom(lager::lenses::attr(&whole::a)) .zoom(lager::lenses::at(0)) .zoom(lager::lenses::value_or("no value")); For convenience, one can also use the ``operator[]``, which takes a lens, key (index) or pointer to attribute. The latter two will be converted into a lens using ``lager::lenses::at`` and ``lager::lenses::attr`` automatically. The example above can also be written as: .. code-block:: c++ lager::cursor part_cursor = state[&whole::p]; lager::cursor map_cursor = state[&whole::m]; lager::cursor int_cursor = map_cursor["foo"][lager::lenses::or_default]; lager::cursor str_cursor = state[&whole::a][0][lager::lenses::value_or("no value")]; .. _transformations: Transformations --------------- The ``xform()`` function is another way to transform the cursor. For read-only cursors (``lager::reader``), it takes one transducer (see `zug`_ for more information); for writable cursors (``lager::writer`` and ``lager::cursor``), it can take two to transform into another writable cursor, or take one to transform into a read-only cursor. .. _zug: https://github.com/arximboldi/zug .. code-block:: c++ lager::reader str = ...; // One-way transformation for read-only cursors lager::reader str_length = str.xform( zug::map([](std::string x) { return x.size(); })); lager::cursor str = ...; // Two-way transformation for writable cursors lager::cursor num = str.xform( zug::map([](std::string x) { return std::stoi(x); }), zug::map([](int x) { return std::to_string(x); }) ); // One-way transformation to make a read-only cursor lager::reader num2 = num.xform( zug::map([](int x) { return 2*x; })); str.set("123"); // You need `lager::commit(state);` // if you use transactional_tag std::cout << num.get() << std::endl; // 123 num.set(42); std::cout << str.get() << std::endl; // 42 std::cout << num2.get() << std::endl; // 84 .. _combinations: Combinations ------------ You can combine more than one cursors into one using ``with()``. The resulted cursor will be of a ``std::tuple`` containing all the value types in the original cursors: .. code-block:: c++ #include lager::cursor num = ...; lager::cursor str = ...; lager::cursor> dual = lager::with(num, str); // If any of the cursors passed into with() are read-only, // it will result in a read-only cursor. lager::reader str_ro = ...; lager::reader> dual_ro = lager::with(num, str_ro); .. _using-cursors: Using cursors ------------- We will use a minimal example to show how cursors work. Suppose one wants to represent a house using the following models, actions and reducers: .. code-block:: c++ #include struct room { bool light_on; }; struct toggle_light_action {}; using room_action = std::variant; room update_room(room r, room_action a) { return std::visit(lager::visitor{ [=](toggle_light_action) { return room{ ! r.light_on }; } }, a); } struct house { immer::map rooms; }; struct change_room_action { std::string id; room_action a; }; struct add_room_action { std::string id; room r; }; using house_action = std::variant; house update(house h, house_action a) { return std::visit(lager::visitor{ [&](change_room_action a) { auto old_room = h.rooms[a.id]; auto new_room = update_room(old_room, a.a); // For simplicity we do not add move semantics // here, but you should do in your own program h.rooms = h.rooms.set(a.id, new_room); return h; }, [&](add_room_action a) { h.rooms = h.rooms.set(a.id, a.r); return h; } }, a); } Create the single source of truth ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As discussed above, we have two choices for our single source of truth: ``lager::state`` or ``lager::store``. If you are refactoring old code, it may be a good choice to use ``lager::state`` because it allows you to gradually lift the state up without rewriting everything at once. If you are developing new software, it may be worthy to to use ``lager::store`` to benefit from the use of actions. Here, we will use ``lager::store`` as an example. .. code-block:: c++ #include #include // Make an initial model house initial_house; initial_house.rooms = initial_house.rooms .set("kitchen", room{false}); initial_house.rooms = initial_house.rooms .set("bedroom", room{true}); auto store = lager::make_store( initial_house, // Be sure to use a suitable event loop // that integrates into the rest of your program lager::with_manual_event_loop{}); Zooming the cursors ~~~~~~~~~~~~~~~~~~~ Suppose we want to access and watch the state of the kitchen. We can use the ``zoom()`` method to obtain a cursor just for that: .. code-block:: c++ #include #include #include lager::reader kitchen_cursor = store .zoom(lager::lenses::attr(&house::rooms)) .zoom(lager::lenses::at("kitchen")) // maybe you want to use some other // approach to deal with this std::optional .zoom(lager::lenses::or_default); // You can now query for the state: auto kitchen = kitchen_cursor.get(); auto kitchen_light_on = kitchen.light_on; Using cursors in object-oriented views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Suppose we want to display our room in an object-oriented GUI library, we can make the widget receive a cursor to the room model and watch it for changes: .. code-block:: c++ class room_component : public widget { lager::reader r; lager::reader light_on; label l; static std::string light_state(bool on) { return on ? "light is on" : "light is off"; } public: room_component(lager::reader r, widget *parent = 0) : widget(parent) , r(r) , light_on(r[&room::light_on]) , l(light_state(light_on.get())) { lager::watch(light_on, [&] (bool on) { l.set_text(light_state(on)); }); } }; .. note:: A ``lager::watch(reader, ...)`` is bound to the ``reader`` object. It is simply an alias of ``reader.watch(...)``. This means that when the reader object goes out of scope, the watch is disposed. For example: .. code-block:: c++ void setup_watch() { auto reader = my_store[&my_model::foo]; lager::watch(reader, [] (auto value) { std::cout << value << std::endl; }); } Because the reader is freed when ``setup_watch()`` returns, the watch is disposed and will never get called. Instead store the reader object in a class member or somewhere else where it lives for at least as long as the watch is necessary. Dispatching actions ~~~~~~~~~~~~~~~~~~~ Of course, we do not want the GUI to only display the model. Instead, we would like to allow it make changes to our model. Here, since we are using ``lager::store`` as our single source of truth, we benefit from making changes through actions. We dispatch actions through contexts. Here, ``lager::store`` is a context. We may directly dispatch actions via the store: .. code-block:: c++ store.dispatch(change_room_action{"kitchen", toogle_light_action{}}); But for the ``room_component`` we have here, it may not be a great idea, because it breaks modularity. If we were to dispatch an action via ``store``, the room component will need to know the room's id. In other words, it has to know something about the house, rather than only know about the room itself. We would like to have a context that can dispatch a ``room_action``, instead of a ``house_action``: .. code-block:: c++ ctx.dispatch(toogle_light_action{}); // what should ctx be? Fortunately, lager provides a context conversion constructor that can be used here, and the only thing we would like to do is to provide a conversion function that converts a ``room_action`` into a ``house_action``: .. code-block:: c++ std::string room_id = "kitchen"; auto ctx = lager::context( store, [=](room_action a) -> house_action { return change_room_action{ room_id, a }; }); And now we can add a toggle button to our room component to control the light: .. code-block:: c++ class room_component : public widget { lager::reader r; lager::reader light_on; lager::context ctx; label l; button b; static std::string light_state(bool on) { return on ? "light is on" : "light is off"; } public: room_component(lager::reader r, lager::context ctx, widget *parent = 0) : widget(parent) , r(r) , light_on(r.zoom(lager::lenses::attr(&room::light_on))) , ctx(ctx) , l(light_state(light_on.get())) , b("Toogle light") { lager::watch(light_on, [&](bool on) { l.set_text(light_state(on)); }); b.clicked.connect([ctx=this->ctx]() { ctx.dispatch(toogle_light_action{}); }); } }; .. _additional-resources: Additional resources -------------------- To learn more about cursors, you can watch the **C++ Russia 2019 Talk**: `Squaring the circle: value oriented design in an object oriented system `_ (`slides`_). .. _slides: https://sinusoid.es/talks/cpprussia19-piter lager-0.1.1/doc/debug.rst000066400000000000000000000004441451275761400152060ustar00rootroot00000000000000 debug ===== with_debugger ------------- .. doxygenfunction:: lager::with_debugger debugger -------- .. doxygenstruct:: lager::debugger tree_debugger ------------- .. doxygenstruct:: lager::tree_debugger http_debug_server ----------------- .. doxygenclass:: lager::http_debug_server lager-0.1.1/doc/deps.rst000066400000000000000000000003621451275761400150520ustar00rootroot00000000000000 deps ==== deps ---- .. doxygenclass:: lager::deps make_deps --------- .. doxygenfunction:: lager::make_deps specs ----- .. doxygennamespace:: lager::dep :content-only: access ------ .. doxygengroup:: deps-access :content-only: lager-0.1.1/doc/doxygen.config000066400000000000000000000011151451275761400162260ustar00rootroot00000000000000PROJECT_NAME = "lager" OUTPUT_DIRECTORY = _doxygen GENERATE_LATEX = NO GENERATE_MAN = NO GENERATE_RTF = NO GENERATE_HTML = NO GENERATE_XML = YES INPUT = \ ../lager \ ../lager/event_loop \ ../lager/lenses \ ../lager/debug INCLUDE_PATH = .. QUIET = YES JAVADOC_AUTOBRIEF = YES ENABLE_PREPROCESSING = YES MARKDOWN_SUPPORT = YES MACRO_EXPANSION = YES ALIASES = "rst=\verbatim embed:rst:leading-asterisk\n" ALIASES += "endrst=\n\endverbatim" ALIASES += "rst{1}=\rst\1\endrst" lager-0.1.1/doc/effects.rst000066400000000000000000000232021451275761400155340ustar00rootroot00000000000000 .. _effects: Effects ======= Effects allow :ref:`reducers` to request the execution of code that would break the :ref:`purity` of the reducer . In this way, we can describe I/O and other interactions directly from the reducer, without directly executing them. Writing effects --------------- The type :ref:`lager::effect` is defined in the library as: .. code-block:: c++ template > using effect = std::function&)>; This is, an *effect* is just a procedure that takes some :cpp:class:`lager::context` as an argument. The *context* is parametrized over an ``Action`` type. This allows the effect to deliver new actions by using the :cpp:func:`lager::context::dispatch` method. For example, if you tried to write a file in an effect, you probably want to dispatch actions to inform the system about whether the operation succeeded or failed. The context also has an optional ``Deps`` parameter, which allows the effect to use a :ref:`dependency injection` mechanism to get access to *services* that it might need to perform the side effects. To produce an effect, you can just return it from the reducer. In the previous section we learnt that :ref:`reducers` are functions with a signature of the form: .. code-block:: c++ auto update(Model, Action) -> Model; However, a reducer may alternatively be a function of the form: .. code-block:: c++ auto update(Model, Action) -> std::pair>; The :cpp:class:`lager::store` automatically detects at compile time whether the given reducer returns effects. If it does, it schedules the excution of the effects in the main loop, directly after the evaluation of the update function. .. note:: Technically the effectful reducer does not need return a ``std::pair``, but anything that defines ``std::get`` and returns a model at index ``0`` and an effect at index ``1``. This means that you can use an ``std::tuple`` instead, for example. Also, the effect does not need to match the type exactly, it just needs to be convertible to it. .. _intent-effect-example: A minimalist example -------------------- The simplest useful effect is an effect that simply delivers another action. In architectural introduction we introduced the notion of :ref:`intent`, a function that maps actions expressed in UI language to actions expressed in application logic language. Later, we showed :ref:`an example ` of a reducer that expresses *intent* by directly calling a reducer with the derived action. However, sometimes one might prefer to have the reducer return to the main loop before delivering the new action. This allows Lager to see the newly generated action. This means that this action can be inspected in the :ref:`time travelling debugger` and other tools. The :ref:`previous example ` can thus be rewritten using a reducer that, when it receives a UI logic action, communicates intent by returning an effect that delivers an application logic action. .. code-block:: c++ #include using action = std::variant; using todos_result = std::pair>; todos_result update(todos_model m, action a) { return std::visit(lager::visitor{ [] (const todos_action& a) -> todos_result { return {update(m, a), lager::noop}; }, [] (const todos_command& c) -> todos_result { static const auto command_actions = std::map>{ "add", [] (auto arg) { return add_todo{arg}; }, "remove", [] (auto arg) { return remove_todo{std::stoi(arg)}; }, "toggle", [] (auto arg) { return toggle_todo{std::stoi(arg)}; }, }; auto it = command_actions.find(c.command); if (it == command_actions.end()) return {m, lager::noop}; else { auto new_action = it->second(c.argument); return {m, [] (auto&& ctx) { ctx.dispatch(new_action); }}; } }, }, a); } Note how we use a ``std::variant`` to combine the *business logic* action type (``todo_action``) with the *UI logic* action (``todos_command``). When we receive a business logic action, we just forward to its reducer. When we receive a UI level action, we figure out whether this action should result in a business logic action, and if so, we deliver it via an effect. .. note:: We used :ref:`lager::noop` as an *empty* effect in the paths that do not require one. It can be more efficient than using ``[] (auto&&) {}`` because Lager detects it, completely bypassing the evaluation of the effect. .. _dependency-injection: Dependency passing ------------------ Oftentimes, the effect will need to access some *service* in order to do its deed. For example, when performing asynchronous IO it may need to access some `boost::asio::io_context`_. Or maybe the effect should forward to some other service of your own that encapsulates the I/O logic. By definition, these types are referential, so you can not put them in the model or the action that is passed to the reducer that generates the effect. .. _boost\:\:asio\:\:io_context: https://www.boost.org/doc/libs/1_70_0/doc/html/boost_asio/reference/io_context.html Instead, the framework can deliver these services for you by declaring the dependency in the effect signature using the :cpp:class:`lager::deps` type. For example, an effect that wants a reference to a ``boost::asio::io_context`` can be declared like this: .. code-block:: c++ lager::effect> eff = [] (auto&& ctx) { auto& io = get(ctx); io.post([] { ... }); // for example }; If the *reducer* returns such an effect, the store will pass the dependencies to it, embeded in the context. The ``get()`` function is used to access the dependencies inside the effect. For this to work, we need to provide the dependencies to the store by using :cpp:func:`lager::with_deps` as an extra argument to the :cpp:func:`lager::make_store` factory: .. code-block:: c++ auto io = boost::asio::io_context{}; auto store = lager::make_store( // ... lager::with_deps(std::ref(io))); If you fail to provide all the dependencies required for the effects, there will be a compilation error. .. warning:: Dependencies are passed by value by default. Use ``std::ref`` to mark the dependencies that shall be passed by reference. Identifying dependencies ~~~~~~~~~~~~~~~~~~~~~~~~ In the previous example, there is ony one instance of ``boost::asio::io_context`` that is passed to all the effects that are evaluated within the Lager context. This is a good replacement of the `singleton design pattern`_: there is a single instance, but there is low physical coupling and you can still replace the instance in a different context, particularly in unit tests. .. _singleton design pattern: https://en.wikipedia.org/wiki/Singleton_pattern However, we often will need to have multiple instances of a type provided to different subsystems. We can declare type tags to differentiate these instances using these types as keys: .. code-block:: c++ struct foo_dep {}; struct bar_dep {}; lager::effect> foo_effect = [] (auto&& ctx) { auto& io = get(ctx); // ... }; lager::effect> bar_effect = [] (auto&& ctx) { auto& io = get(ctx); // ... }; Now we can provide two separate ``io_context`` instances for the two subsystems: .. code-block:: c++ using boost::asio::io_context; auto foo_io = io_context{}; auto bar_io = io_context{}; auto store = lager::make_store( // ... lager::with_deps( lager::dep::as>(foo_io), lager::dep::as>(bar_io))); Using this technique, the names of the dependencies are still static. This makes the mechanism very efficient and ensures that dependency mismatches are cought at compile time. If the number of instances of a dependency is determined dynamically, you will need to define a kind of *manager* for these instances and provide this manager as a service instead. This naturally involves defining a :ref:`identity` scheme for these dependencies, such that the effects that required them can refer to them. Other dependency specs ~~~~~~~~~~~~~~~~~~~~~~ The type :cpp:type:`lager::dep::key` is a *dependency specification*, used to describe how the dependency is required. There are other specifications, and they can be combined: - :cpp:type:`lager::dep::opt` is used to notate **optional** dependencies. There won't be a compilation error if a depedency is missing when a module requests it as optional. Instead, get may throw an exception. You can check if the dependency is provided using :cpp:func:`lager::has`. - :cpp:type:`lager::dep::fn` is used to notate **lazy** dependencies. These may not be available directly when building the store, but are instead to be requested lazily through a provided function. lager-0.1.1/doc/event_loop.rst000066400000000000000000000010361451275761400162700ustar00rootroot00000000000000 .. _event_loop event_loop ========== manual ------ .. doxygenstruct:: lager::with_manual_event_loop queue ----- .. doxygenstruct:: lager::queue_event_loop .. doxygenstruct:: lager::with_queue_event_loop boost_asio ---------- .. doxygenstruct:: lager::with_boost_asio_event_loop sdl --- .. doxygenstruct:: lager::sdl_event_loop .. doxygenstruct:: lager::with_sdl_event_loop qt ---- .. doxygenstruct:: lager::with_qt_event_loop qml ---- .. doxygenclass:: lager::event_loop_quick_item .. doxygenstruct:: lager::with_qml_event_loop lager-0.1.1/doc/index.rst000066400000000000000000000007151451275761400152300ustar00rootroot00000000000000 .. include:: ../README.rst :end-before: include:index/end Contents -------- .. toctree:: :caption: User Manual :maxdepth: 3 introduction architecture model actions reducers effects views modularity cursors lenses time-travel .. toctree:: :caption: Reference :maxdepth: 3 context cursor debug deps event_loop lens store util ---- * :ref:`genindex` * :ref:`modindex` * :ref:`search` lager-0.1.1/doc/introduction.rst000066400000000000000000000001431451275761400166350ustar00rootroot00000000000000 Introduction ============ .. include:: ../README.rst :start-after: include:introduction/start lager-0.1.1/doc/lens.rst000066400000000000000000000002151451275761400150550ustar00rootroot00000000000000 lens ==== .. doxygengroup:: lenses-api :project: lager :content-only: .. doxygengroup:: lenses :project: lager :content-only: lager-0.1.1/doc/lenses.rst000066400000000000000000000363561451275761400154240ustar00rootroot00000000000000.. _lenses: Lenses ====== A tool borrowed from functional programming. While lenses are used extensively in Lager for zooming on cursors, they are also useful on their own for manipulating immutable data. .. _making-a-lens: Making a lens ------------- Lenses are, conceptually, a pair of functions for focusing on a part of a whole. You use a lens with the following interface: .. code-block:: c++ // pseudocode for the lens interface: part view(lens, whole); // get part of a whole whole set(lens, whole, part); // set the part of a whole whole over(lens, whole, std::function); // update the part of a whole lens compose(lens, lens); // compose two lenses into another lens Let's use this mouse as our model: .. image:: _static/mouse.svg :width: 50% :align: center .. code-block:: c++ struct Mouse { Mouth mouth; pair eyes; vector legs; vector whiskers; Tail tail; }; So, how do we make a lens, say, to access the eyes of a mouse? .. code-block:: c++ auto eyes = [](auto f) { return [f](Mouse mouse) { return f(mouse.eyes)([&](pair eyes) { mouse.eyes = eyes; return mouse; }); }; }; This is a `van Laarhoven lens`_, which is a bit difficult to understand at first glance. Thankfully, we provide a way to generate this kind of construct with a pair of functions: .. code-block:: c++ auto eyes = lager::lenses::getset( // the getter (Mouse -> Eyes) [](Mouse mouse) { return mouse.eyes; }, // the setter (Mouse, Eyes -> Mouse) [](Mouse mouse, pair eyes) { mouse.eyes = eyes; return mouse; }); Now, anyone can make their own lenses without knowing all the gritty metaprogramming details. Of course, writing all of these by hand is kind of a pain, so we also provide a set of lens generators for a few common patterns: .. code-block:: c++ #include auto eyes = lager::lenses::attr(&Mouse::eyes); ``attr`` will generate a lens from a pointer to member. We will go over the rest of these generators later on. .. admonition:: Note :class: note The main takeaway from this is that lenses are just *pure functions*. .. _van Laarhoven lens: https://www.twanvl.nl/blog/haskell/cps-functional-references .. _composition: Composition ----------- Lenses are a fairly "new" (2007) concept, even in functional programming. One of the main struggles functional programmers faced with them is composition: back when lenses were known as `Accessors`_, lens composition was a mess to write... Thankfully, functional programmers have since found increasingly clean ways of doing lens composition, starting with `Twan van Laarhoven's implementation`__, and many more to come. If you're curious about the canonical way of doing "Optics" (a superset of lenses), I invite you to read about `Profunctor Optics`_. __ `van Laarhoven lens`_ So how does all of this affect us? Simple: **lens composition** with VLLs (van Laarhoven lenses) **is function composition**! .. code-block:: c++ #include auto eyes = lager::lenses::attr(&Mouse::eyes); auto first = lager::lenses::attr(&pair::first); auto first_eye = [=](auto f){ return eyes(first(f)); }; Now, because doing function composition in C++ is unfortunately a bit verbose, we provide syntactic sugar for function composition through ``zug::comp``: .. code-block:: c++ #include // all of these are equivalent: auto first_eye = [=](auto f){ return eyes(first(f)); }; auto first_eye = zug::comp(eyes, first); auto first_eye = eyes | first; .. admonition:: Zug `Zug`_ is a C++ transducer implementation. It is used behind the scenes in Lager, but you can also use it for writing cursor transformations. It also has a few utilities you might find useful. ``zug::comp`` is one of those. ``zug::comp`` does two things: it is able to compose any number of functions, and it wraps them so that you can use the pipe operator to compose them with any other function. All the lens generators in lager (including getset) wrap their results in a ``zug::comp``, so you can use the *pipe operator* to *compose lenses* together. Let's look at an example of this in action: our mouse's mouth has four incisors! .. code-block:: c++ struct Mouth { using ToothPair = pair; // lower pair and upper pair! pair incisors; }; Say our mouse has a bad tooth, and we need to replace it. .. code-block:: c++ Mouse replace_tooth(Mouse mouse, Tooth tooth) { auto tooth_lens = attr(&Mouse::mouth) | attr(&decltype(Mouth::incisors)::first) | attr(&Mouth::ToothPair::first); return set(tooth_lens, mouse, tooth); } Another thing you might notice, is that *the identity for lens composition is the identity function!* .. code-block:: c++ auto add4 = [](int x) { return x + 4; }; over([](auto f) { return f; }, 11, add4) // using our own identity function over(zug::indentity, 11, add4) // using zug's identity function struct Foo { int value; }; view(zug::identity | attr(&Foo::value), Foo{42}); view(attr(&Foo::value) | zug::identity, Foo{42}); .. _zug: https://sinusoid.es/zug/ .. _accessors: http://web.archive.org/web/20071023064034/http://luqui.org/blog/archives/2007/08/05/haskell-state-accessors-second-attempt-composability/ .. _profunctor optics: https://www.cs.ox.ac.uk/people/jeremy.gibbons/publications/poptics.pdf .. _lens-generators: Lens generators --------------- Let's look at the different lens generators that are available to us. Assume the following is available: .. code-block:: c++ #include using namespace lager; using namespace lager::lenses; Mouse mouse; // our instance of a mouse We've already seen ``attr``: .. code-block:: c++ #include auto first_eye = attr(&Mouse::eyes) | attr(&pair::first); Eye eye = view(first_eye, mouse); ``at`` is an accessor for an element of a collection at an index (integers for sequences like ``vector``, keys for associative collections like ``map``): .. code-block:: c++ #include auto first_whisker = attr(&Mouse::whiskers) | at(0); optional maybe_whisker = view(first_whisker, mouse); Note that the focus (``part``) of at is an optional. That's because *the focused element might be absent* (out of bounds, no value at key, etc). We'll go over handling optionals later. If you don't want to handle optionals and you're ok with using default constructed values as a representation of the absence of focus, you can use ``at_or``: .. code-block:: c++ #include // default constructing a value if none is present: auto with_default = attr(&Mouse::whiskers) | at_or(0); // using a fallback value: Whisker fallback_whisker; auto with_fallback = attr(&Mouse::whiskers) | at_or(0, fallback_whisker); auto first_whisker = with_default; Whisker whisker = view(first_whisker, mouse); This is *usually* not recommended, please use ``at`` and handle optionals properly. Then there's handling variants: .. code-block:: c++ #include variant rodent; auto the_mouse = alternative; optional maybe_mouse = view(the_mouse, rodent); Similarly to ``at``, ``alternative``'s focus is an optional. Finally because `recursive types should be implemented with boxes `_, we provide unbox: .. code-block:: c++ #include // a tail node has a position and maybe another tail node struct Tail { int position; box> tail; }; auto tail = attr(&Mouse::tail) | attr(&Tail::tail) | unbox; optional maybe_tail = view(tail, mouse); Note that tail really should be of type ``optional>``, but for that we'd need to handle composing with optionals. .. _handling-optionals: Handling optionals ------------------ So many optionals everywhere! How do we compose lenses that focus on optionals? This is the part that gets slightly tricky: you can't compose a lens that focuses on an optional with a lens that expects a value. But you can *turn a lens that expects a value into a lens that expects an optional!* We provide three ways of doing this. Assume the following is available: .. code-block:: c++ #include #include #include #include using namespace lager; using namespace lager::lenses; struct Mouse; // from earlier struct Digit { int position; }; struct Leg { int position; vector digits; }; Mouse mouse; // our instance of a mouse The first one is ``map_opt``: .. code-block:: c++ auto leg_position = attr(&Leg::position); auto first = at(0); auto first_leg_position = attr(&Mouse::legs) // vector | first // optional | map_opt(leg_position); // optional optional position = view(first_leg_position, mouse); ``map_opt`` turned our ``lens`` into a ``lens, optional>``. This is one way to lift lenses to handle optionals. Now, what happens if we try to do the same thing to get the first ``Digit`` of the first ``Leg``? .. code-block:: c++ auto digits = attr(&Leg::digits); auto first = at(0); auto first_digit = attr(&Mouse::legs) // vector | first // optional | map_opt(digits) // optional> | map_opt(first); // optional> optional> digit = view(first_digit, mouse); Oh no. We got an optional of optional, which is not what we wanted. We wanted to turn our ``lens, optional>`` into a ``lens>, optional>``. For this, we have ``bind_opt``: .. code-block:: c++ auto first_digit = attr(&Mouse::legs) // vector | first // optional | map_opt(digits) // optional> | bind_opt(first); // optional optional digit = view(first_digit, mouse); Note that you can lift composed lenses too! .. code-block:: c++ auto first_digit = attr(&Mouse::legs) // vector | first // optional | bind_opt(digits | first); // optional ``bind_opt`` collapses two levels of optional into one, much like the monadic bind of the `Maybe Monad`_ (don't think too much about it). For convenience, we also provide ``with_opt``, which will automatically attempt to collapse two levels of optionals if it finds any: .. code-block:: c++ auto first_digit = attr(&Mouse::legs) // vector | first // optional | with_opt(digits | first); // optional optional digit = view(first_digit, mouse); auto first_leg_position = attr(&Mouse::legs) // vector | first // optional | with_opt(leg_position); // optional optional position = view(first_leg_position, mouse); This should be safe to use, but be weary of using it with models that have optionals as legitimate values. Using the less ambiguous ``map_opt`` and ``bind_opt`` is preffered. Of course, we also provide a lens for falling back to either a default constructed value or a fallback value with ``value_or`` and ``or_default``: .. code-block:: c++ auto first_leg_position = attr(&Mouse::legs) // vector | first // optional | map_opt(leg_position); // optional auto with_default = first_leg_position | or_default; // default constructed // auto with_default = first_leg_position | value_or(); // equivalent auto with_fallback = first_leg_position | value_or(-1); // fallback to -1 int position = view(with_fallback, mouse); .. _maybe monad: https://en.wikipedia.org/wiki/Monad_(functional_programming) .. _dynamic-lenses: Dynamic lenses -------------- You've probably noticed that all of our lenses have the type ``auto`` in the previous examples. This is because VLLs rely on *compile-time type information* to implement ``view``, ``set`` and ``over``, and the resulting types are somewhat cryptic... This is fine for composing lenses at compile time, but here's the catch: .. code-block:: c++ struct Tail { int position; optional> tail; }; auto tail = attr(&Tail::tail) | value_or() | unbox; auto position = attr(&Tail::position); auto lens1 = tail | position; // lens auto lens2 = tail | tail | position; // lens static_assert(std::is_same_v, "Not the same types!"); This means that you can't have this kind of pattern: .. code-block:: c++ auto tail_position_at(int index) { auto result_lens = position; while(index-- > 0) { result_lens = tail | result_lens; // won't compile, the type changed! } return result_lens; } We need a way to store ``lens1`` and ``lens2`` in the same type, because they satisfy the same interface that we defined in `Making a lens`_ (they are both, conceptually, ``lens``). This is where *type erasure* comes in: .. code-block:: c++ #include // type erased lenses lens tail_position_at(int index) { lens result_lens = position; while (index-- > 0) { result_lens = tail | result_lens; // this works now } return result_lens; } The ```` header provides a type erased lens for this very purpose. This is achieved through the same technique used for implementing ``std::function``. .. admonition:: Virtual dispatch overhead :class: warning Type erased lenses are less performant at runtime, because of virtual dispatch, and because we can't take advantage of a number of optimizations done by VLLs. For this reason, **do not use type erased lenses if you can express something equivalent at compile time**. (``std::function`` suffers from similar limitations, and as such follows the same recommendations) Let's reimplement that last function one last time, with proper handling of optionals this time: .. code-block:: c++ auto tail = attr(&Tail::tail) | map_opt(unbox); auto position = attr(&Tail::position) | force_opt; lens> tail_position_at(int index) { lens> result_lens = position; while (index-- > 0) { result_lens = tail | bind_opt(result_lens); } return result_lens; } Notice that we introduced ``force_opt``. This is so that we can keep the return type as ``lens>``, even in the case of a single node tail. lager-0.1.1/doc/model.rst000066400000000000000000000407721451275761400152300ustar00rootroot00000000000000 .. _model: Model ===== The data model design is key in a Lager application. The good news is: changing the design and shape of the data-model in a value-oriented world tends to be easy. However, for this to be the case, it is important that we follow some principles strictly. .. _value-semantics: .. _independence-principle: Value semantics --------------- In the previous section we emphasised that the data-model **must be a value type**. But what is a `value type`_ really? In a purely `functional programming`_ language, like Haskell, everything you name is a *value*. This is, all entities are like mathematical objects: they are immutable and have some definition of equivalence. In a multi-paradigm language like C++, you have both *value types* and *reference types*. Intances of value types define its meaning through the values that they represent, that is why we say, they have *value semantics*. Each instance of a value type has its own copy of a representation of a value, on which we can operate independently. Most of the time, we do not care about the identity of an instance, but about the content, the value, which can be *equal* between instances. *Reference types*, however, reference other mutable objects. The identity of these objects is crucial. We can observe changes to the referred objects through the references. This means that we have to be particularly careful when dealing with reference types. The canonical reference types in C++ are references (``&``) and pointers (``*``). There are many other reference types. For example, iterators of a container are references that point into the container. If we change the container, we can observe the changes through the iterator. Sometimes even, the iterator may become invalid. In C++, instances of value types are not necessarily immutable, only the *value* they represent is. For example, I can mutate a `std::vector`_ through its ``push_back()`` method. Or I can modify a ``struct`` by assigning to its members. Instead, *they are value types because each instance of the type is independent and is a attributed meaning through the values it represent.* This **principle of independence** is highlighted by this code snippet: .. code-block:: c++ auto a = std::vector{}; auto b = a; foo(b); assert(a.empty()); Here we have an instance of an empty ``std::vector`` named ``a`` and copy it into another instance ``b``. Then we call ``foo()`` and finally make an assertion about the state of ``a``. We do not need to know anything about what ``foo()`` does to reason about the state of ``a``. It could have taken ``b`` by reference and changed it, maybe even from another thread---it does not matter. In any case, we can be sure that our assertion about ``a`` holds, because ``b`` is an independent copy of it, so we can not observe any change in ``b`` through ``a``. This frees our mind, allowing us to reason locally about the state of the program. This is fundamental to write correct concurrent programs: we can make sure each thread works on their local copy of the data to ensure the absence of race conditions. .. _value type: https://en.wikipedia.org/wiki/Value_type_and_reference_type .. _std::vector: https://en.cppreference.com/w/cpp/container/vector .. _functional programming: https://en.wikipedia.org/wiki/Functional_programming .. admonition:: Spotting reference types In order to avoid them, it is important to be able to clearly identify *reference types*. They are sneaky. For example, ``std::vector`` is a value type only as long as ``T`` is also a value type. Same applies to ``struct``, it is not a value as soon as a member is not. Also, there are some C++ types that behave a bit like values, they seem to be copyable at least---but their copies reference external entities that mutate, and these changes can be observed through all these shallow copies! Here are some examples of such types that shall be avoided in our data-model: * A **pointer**. A pointer has a value, which is the memory location of an object. But we can also use this pointer value to change and observe changes in this memory location. * An **iterator** into a container, as mentioned above. * A **file handle**. The file changes independently and we can use the handle to observe or make changes. The file can even change outside of our program, completely outside of our control. * Most **resource handles** actually. Ids of OpenGL buffers, Posix process handles, etc. Lots of these things look like an ``int``, but they are not, they are a reference to a mutable entity. * **Function objects** that capture references, particularly lambdas with ``&``-captures. **Are pointers always bad?** Not always. For instance, the *implementation* of ``std::vector`` uses pointers internally. But it defines ``operator=`` to ensure the independence principle. Also, Sean Parent shows in his famous talk `"Inheritance is the base class of evil"`_ that given a value type ``T``, a forest of ``std::shared_ptr`` is effectively a value type. However, the ``immer::box`` type from the Immer_ library serves the same purpose with safer and more convenient interface. In the :ref:`performance` section we will cover the power immutability in more detail. In general, the application level data model must never use pointers. It can however use generic container types that encapsulate the pointers and properly deal with the intricacies of defining ``operator=`` as to ensure the independence principle. .. _"Inheritance is the base class of evil": https://www.youtube.com/watch?v=bIhUE5uUFOA .. _immer: https://github.com/arximboldi/immer .. admonition:: Further reading :class: note Value-semantic thinking is a vast topic. Here are some resources that can help you adopt this design mindset: * The book `Elements of Programming`_, by `Alexander Stepanov`_, author of the STL and great thinker on the intersection between programming and math. * The talk `Better Code: Data Structures`_ by `Sean Parent`_, author of the Adobe Source Libraries, famous for popularizing `type erasure`_ for value based runtime polymorphism. * The talk `The most valuable values`_, by `Juan Pedro Bolívar Puente`_, author of this library. In the functional programming realm these ideas are taken much further: * The talk `The value of values`_, and the supporting essay `Values and Change`_, by `Rich Hickey`_, author of the programming language Clojure. * The talk `Denotational Design: from meanings to programs`_, where `Conal Elliott`_, inventor of `functional reactive programming`_ among many other other things, talks about applying mathematical thinking to the design of programs. * The book `Functional Programming in C++`_ by `Ivan Čukić`_, which shows how C++ not only supports value semantics, but the functional programming paradigm as a whole. .. _elements of programming: http://elementsofprogramming.com .. _alexander stepanov: https://en.wikipedia.org/wiki/Alexander_Stepanov .. _better code\: data structures: https://www.youtube.com/watch?v=sWgDk-o-6ZE .. _sean parent: https://sean-parent.stlab.cc/ .. _the most valuable values: https://www.youtube.com/watch?v=_oBx_NbLghY .. _the value of values: https://www.youtube.com/watch?v=-6BsiVyC1kM .. _values and change: https://clojure.org/about/state .. _juan pedro bolívar puente: http://sinusoid.al .. _denotational design\: from meanings to programs: https://www.youtube.com/watch?v=bmKYiUOEo2A .. _functional programming in c++: https://www.manning.com/books/functional-programming-in-c-plus-plus .. _Ivan Čukić: https://cukic.co/ .. _conal elliott: http://conal.net/ .. _functional reactive programming: https://en.wikipedia.org/wiki/Functional_reactive_programming .. _type erasure: https://www.youtube.com/watch?v=QGcVXgEVMJg .. _rich hickey: https://twitter.com/richhickey .. _identity: Identity -------- .. image:: _static/identity.png :align: center When writing the model as value types, we soon encounter the problem of dealing with **identity**. Consider an interactive application shows a moving person. This person *changes*, it moves around. Our model would be a *snapshot* of the *state* of this person. But clearly, the *state* of the *person* is different than the person itself: * The **same** person can be in different states, this is, these state values are ``!=``. * Two **different** people can be in the same state, this is, their state values are ``==``. In Object Oriented programming, we normally *identify* a language object with the entity it represents, in this case, the person. The *identity* of the thing is the memory location of the storage for its state. This means that we need to use mutation to deal with the state, that there is only one state per entity at time, that time only progresses in one direction, that change is an implicit construction, that entities can not be dealt with concurrently. Identity becomes an implicit and flaky construction. However, in real life, we deal with identity in an explicit way. That is why people have *names* or *passport numbers*. These are special values, **identity values**, that help us identify people. Identity as such serves a double purpose, solving the forementioned state/identity problems: * *Identity* allows us to recognise *different states* as belonging to *the same entity*. For example, when you show up at different institutional offices, you show your id card to show that these are the same person. * *Identity* allows us to *differentiate* to a specific entity, that might have otherwise similar states. In a room full of people, you can call someone by their full name to refer to a particular person univocally, distinguishing them from the group. Considering this duality, when your program deals with changing entities, you will have to think about the domain of entities as a whole, and give each of those entities a different, explicit, identity value. `Universally Unique Identifiers`_ are a powerful tool to identify entities not only in the running programm, but also across files and machines. Often though, context will allow us to have more lightweight identity values. In some cases, even its index in a vector might suffice. .. _universally unique identifiers: https://en.wikipedia.org/wiki/Universally_unique_identifier .. admonition:: References in a value world Consider this data-structure which implements agenda of people with friends in a referential way: .. code-block:: c++ struct person { std::string name; std::string phone_number; std::vector> friends; }; struct agenda { std::unordered_set> people; }; This is not a valid model to use in Lager, because ``person`` and ``agenda`` are reference semantic types. Not only is the identification of memory objects with entities problematic from a conceptual programming sense: there is some extra friction, like having to allocate each person in a separate memory block (instead of having a flat ``std::vector``), and then dealing with the lifetime of those blocks with ``shared_ptr``, ``weak_ptr``, and so on. How do we do references with out pointers then? We use explicit identity values: .. code-block:: c++ :emphasize-lines: 1,7,12 using person_id = std::string; struct person { std::string name; std::string phone_number; std::vector friends; }; struct agenda { std::unordered_map people; }; Now we have decoupled the *identity* (``person_id``) from the *state* (``person``). Whenever we want to know the state for a given person, we can access it through the ``people`` map in the agenda. Whenever we want to refer to a person, like in the list of friends, we use a ``person_id``. We can now have multiple copies of the whole agenda, and compare how a particular person changes as we manipulate it. People are not tied to their representation in memory anymore, so we can be more playful with the data-structures and apply :ref:`data oriented design ` to reach better cache locality and and overall performance! .. _normalization: Normalization ------------- After applying the principle of explicit identity to your program, you might realise this insight: *the data-model of the application starts to look like a data-base!* And you are correct: the model of our application is an in-memory data-base, and the Lager store, combined with reducers and actions, provide a reproducible, logic aware, `event sourced`_ data-store. This means that you can start applying data-base design wisdom to your application, which has been accrued over decades by academics and practitioners. In particular, you may find interesting the notion of `database normalization`_, both the Redux documentation and the Data-Oriented Design book do indeed talk about it: * The `Normalizing State Shape`_ section from the Redux documentation discusses normalization in the context the unidirectional data-flow architecture. * The `Relational Databases`_ section, from the *Data-Oriented Design* book by Richard Fabian, discusses normalization of the in memory model of C++ programs, with special focus on performance. .. _event sourced: https://martinfowler.com/eaaDev/EventSourcing.html .. _database normalization: https://en.wikipedia.org/wiki/Database_normalization .. _normalizing state shape: https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape .. _relational databases: http://www.dataorienteddesign.com/dodbook/node3.html .. _performance: Performance ----------- One strong concern when applying value-semantics for the data-model of big applications is performance. We encourage passing by value around freely, and storing copies of the model values as needed without much concern. This often rises skepticism from experienced developers with an eye for optimization. For big data-models, isn't that gonna be slow, and even explode the memory usage? Not necessarily. In C++, we normally associate value-semantics, and in particular the :ref:`independence principle`, with deep copying. For standard containers like ``std::vector``, it is the case that whenever we pass by value, a new memory object is created where the whole representation of the value is copied into. This is however not a consequence of value-semantics, but a consequence of mutability! If the object that stores the value can mutate arbitrarily, when passed by value, all of its contents must be copied to ensure that the new object does not change when the original changes. However, if the original object is in some way **immutable**, the immutable parts of the representation can be internally shared accross all the "copies" of the value. This property is called *structural sharing* and makes value-semantics very efficient! In the field of `persistent data-structures`_ we can find many examples of containers designed with the *structural sharing* in mind. Today, we have good implementations of some of these data-structures in C++: * Immer_, **immutable data-structures for C++**. * `Postmodern immutable data-structures`_, CppCon'18 talk about Immer. * `Persistence for the masses`_, ICFP'17 paper on immutable data-structures in C++. .. _immer: https://github.com/arximboldi/immer .. _postmodern immutable data-structures: https://www.youtube.com/watch?v=sPhpelUfu8Q .. _persistence for the masses: https://public.sinusoid.es/misc/immer/immer-icfp17.pdf .. _persistent data-structures: https://en.wikipedia.org/wiki/Persistent_data_structure Also, in modern C++ one may often avoid copies altogether by leveraging `copy ellision`_ and `move semantics`_. It is important to familiarize oneself with these concepts. .. _copy ellision: https://en.cppreference.com/w/cpp/language/copy_elision .. _move semantics: https://stackoverflow.com/questions/3106110/what-is-move-semantics In practice, when combining value-oriented design with immutable data-structures, you will find that not only is performance not a problem, but that **your programs are faster**! This is due to the fact that our data-model becomes more compact, with less pointer chasing and better cache locality, and with flatter call stacks and no traversal of forests of listeners, signals and slots. lager-0.1.1/doc/modularity.rst000066400000000000000000000403351451275761400163140ustar00rootroot00000000000000 .. _modularity: Modularity ========== Until now we have learnt how to build simple self-contained components. However, in real world systems, you will have multiple modules that interact with eachother in various ways, and that are often developed by different people or teams. In this section, we will learn how to scale up our Lager based application. Composability ------------- We use the term **module** to refer to a set of :ref:`model`, :ref:`actions`, :ref:`reducers`, and optionally :ref:`effects` and :ref:`views`. This will be the unit of composition in our system. As an example, we will describe a system with two modules, ``foo`` and ``bar``, that are composed into a bigger module, ``app``. Unlike in an object-oriented system, where relationships are often hidden behind callbacks and associations, we will use simple explicit composition: data members and function calls. .. image:: _static/composition.svg :width: 80% :align: center .. admonition:: Horizontal vs vertical physical organization It might be tempting to organize your program in a *horizontal* or *layered* manner. This is, to have separate folders for all your actions, models, reducers and views. If your model is represented in different UI's, maybe belonging to different applications, it might actually make sense to keep the views separate. However, actions, models and reducers are intimatelly tied together, representing different aspects of the same interface. For this reason, it makes sense to keep their definitions close, in the same folder or maybe even in the same file. This is what we call *vertical* modularization. In this way, your code is organized not around arbitrary technical definitions, but around the features of your application. As you scale up your development organization, this will make it easier to work on various features in autonomous cross-functional teams that integrate product management, design, and full stack development. The unidirectional data-flow design proposed by Lager helps building clear interfaces between these modules that reduce friction at the component, cross-team boundary. .. image:: _static/modules.svg :width: 100% :align: center Composing models ~~~~~~~~~~~~~~~~ We can simply compose the models by using data composition, for example, by having the inner models be *members* of the outer model: .. code-block:: c++ struct app_model { foo_model foo; bar_model bar; }; Composing actions ~~~~~~~~~~~~~~~~~ In previous examples, we used ``std::variant`` to combine multiple action types describing individual operations. We can use this mechanism again to compose the children actions into the parent action itself: .. code-block:: c++ using app_action = std::variant< foo_action, bar_action>; Composing reducers ~~~~~~~~~~~~~~~~~~ Now we need to implement a reducer for the parent `app` module. This reducer needs to invoke the nested reducers and integrate their result into the parent state. For example: .. code-block:: c++ app_model update(app_model app, app_action act) { return std::visit(lager::visitor{ [&] (foo_action a) { app.foo = update_foo(app.foo, a); return app; }, [&] (bar_action a) { app.bar = update_bar(app.bar, a); return app; }, }, act); } Composing effects ~~~~~~~~~~~~~~~~~ All the previous reducers do not produce side-effects. But both the ``foo`` and ``bar`` modules could indeed produce :ref:`effects`, by having their reducers specified as follows: .. code-block:: c++ auto update_foo(foo_model m, foo_action a) -> std::pair>>; auto update_bar(bar_model m, bar_action a) -> std::pair>>; Either function can return an effect. This effect would need to be returned back by the reducer of the `app`. However, what is the type of an effect that can be either of the two submodule types? We have to look at the two template parameters of the :cpp:type:`lager::effect` type: - To combine the actions, we use an action type that is a superset of the two action types. In this case, ``app_action``. If we had no such superset type, we could use ``lager::actions`` as a template parameter for the effect, to indicate that we want an effect that can deliver either of the two disjoint action types. - To combine the dependencies, we just have to make sure to list all the dependencies required by both effects. In our example: ``lager::deps``. We can now write the app reducer as: .. code-block:: c++ using app_result = std::pair< app_model, lager::effect>; app_result update(bar_model m, bar_action a) { return std::visit(lager::visitor{ [&] (foo_action a) -> app_result { auto [new_foo, eff] = update_foo(a); app.foo = new_foo; return {app, eff}; }, [&] (bar_action a) -> app_result { auto [new_bar, eff] = update_bar(a); app.bar = new_bar; return {app, eff}; }, }, act); } .. note:: In this case, we had two different paths producing two different effects. It might happen sometimes, that you end up with two effects in the same path that you need to combine. You can use the :cpp:func:`lager::sequence` function for this. It will return the first non empty effect or a combined effect that evaluates all in sequence. It uses the rules above to derive the correct result type. .. _undo: .. _genericity: Genericity ---------- One of the advantages of having homogenous concepts with some general semantics is that one can implement generic modules that compose over an underlying module to extend its functionality. A generic module ~~~~~~~~~~~~~~~~ One example is a ``history`` module that implements *undo* over an underlying document model. The document model has a data model and a set of actions and associated reducer to manipulate it. Our module enhances the document by keeping a history of previous states, and allowing the user to undo or redo changes, or maybe even jump to an arbitrary position in the undo history. We can leverage the value semantics of the model and the purity of the reducer to implement this feature in a general way. First, let's define the actions: .. code-block:: c++ struct undo_action {}; struct redo_action {}; struct goto_action { std::size_t position; }; template using history_action = std::variant< DocumentAction, undo_action, redo_action, goto_action> Note how ``history_action`` is templatized over the underlying document action, which is also included in the action variant. We can now define the model: .. code-block:: c++ template struct history_model { immer::array history; std::size_t position = 0; // construct a history from a document history_model(DocumentModel init = {}) : history{immer::array{{std::move(init)}}} {} // get the current document operator const DocumentModel&() const { return history[position]; } }; Again, we templatized the model over the underlying type. We also implemented conversion from the underlying model that constructs a history with an initial state. The history can also be converted to the underlying document model to obtaining the current state. This allows us to pass the history model directly to views that want to present the current document. Now we can finally define a reducer for our ``history`` module. Note how we need to pass the reducer of the underlying document model. We could avoid this by assuming that the reducer has a specific name, like the conventional ``update()``. It does not harm to be explicit though. We can later use a lambda to bind the particular reducer. .. code-block:: c++ template auto update_history(DocumentReducer&& r, history_model m, history_action a) -> history_model { return std::visit(lager::visitor{ [&] (undo_action a) { return update_history(r, m, goto_action{m.position - 1}); }, [&] (redo_action a) { return update_history(r, m, goto_action{m.position + 1}); }, [&] (goto_action a) { if (a.position >= 0 && a.position < m.history.size()) m.position = a.position; return m; }, [&] (DocumentAction a) { auto doc = r(m, a); if (doc != m) { m.position ++; m.history = m.history.take(m.position).push_back(doc); } return m; }, }, act); } The ``history`` specific actions just manipulate the current position. However, the ``DocumentAction`` handler is of particular interest. We obtain an updated document by evaluating the underlying reducer and, if the document actually changed, whe put the new state in the history. The ``take()`` call discards entries happening after the current position. This is the standard behavior in an editor: after you do some *undos*, you loose the ability to *redo* as soon as you make a new edit. There are other possible ways to handle this case: in our example text editor Ewig_ we use `Emacs style undo`_. .. _Ewig: https://github.com/arximboldi/ewig .. _emacs style undo: https://www.gnu.org/software/emacs/manual/html_node/emacs/Undo.html Dealing with underlying effects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The previous reducer did not deal with :ref:`effects`. This might be a reasonable constraint in this case, since it is unclear how meaningful are side-effects in an undoable document. However, we can still change the reducer to deal with potential side effects. We use the :cpp:func:`lager::invoke_reducer` function to invoke a reducer that may or may not have side effects in a generic way. Note that we now have to inform the generic reducer of potential dependencies that the effects returned by the underlying reducer might need. .. code-block:: c++ template , typename DocumentReducer, typename DocumentModel, typename DocumentAction> auto update_history_with_effects(DocumentReducer&& r, history_model m, history_action a) -> std::pair, lager::effect> { return std::visit(lager::visitor{ [&] (undo_action a) { return update_history(r, m, goto_action{m.position - 1}); }, [&] (redo_action a) { return update_history(r, m, goto_action{m.position + 1}); }, [&] (goto_action a) { if (a.position >= 0 && a.position < m.history.size()) m.position = a.position; return {m, lager::noop}; }, [&] (DocumentAction a) { auto eff = lager::effect{lager::noop}; auto doc = lager::invoke_reducer( r, static_cast(m), a, [&](auto e) { eff = e; }); m.position = m.history.size(); m.history = m.history.take(m.position).push_back(doc); return {m, eff}; }, }, act); } .. tip:: In this implementation we always return an effect type, regardless of whether the underlying reducer uses effects at all---if it doesn't, it will unconditionally be a ``lager::noop``. It is possible however to avoid that, by using the :cpp:class:`lager::has_effect` metafunction to only return an effect if needed. .. _enhancer: Enhancer interface ~~~~~~~~~~~~~~~~~~ Given a concrete ``doc`` module composed of a ``doc_model``, ``doc_action`` and ``update_doc()`` reducer, we can produce a store that uses it, enhanced with the ``history`` functionality: .. code-block:: c++ auto store = lager::make_store>( history_model{}, [] (auto m, auto a) { return update_history(update_doc, m, a); }); It would be nice, however, if we could write instead: .. code-block:: c++ :emphasize-lines: 5 auto store = lager::make_store( doc_model, lager::with_reducer(update_doc), with_history); We can indeed write such a ``with_history`` construction, by using the *enhancer* interface. After passing the model, reducer and view to the :cpp:func:`make_store` function, we can pass as many enhancers as we want. These allow it to extend the store with generic middleware, like our undo history. An *enhancer* is just a function that takes a *store factory* as an argument and returns a new factory with the same signature. It must use the provided factory to produce the store, but it can manipulate its arguments. We can implement the ``with_history`` enhancer as follows: .. code-block:: c++ auto with_history = [] (auto next) { return [=] (auto action, auto model, auto reducer, auto loop, auto deps) { using action_t = typename decltype(action)::type; using model_t = decltype(model); using deps_t = decltype(deps); return next( lager::type_>, history_model{model}, [reducer](auto m, auto a) { return update_history(reducer, m, a); }, loop, deps); }; }; .. warning:: To avoid syntactic noise, we did not use `perfect forwarding`_ in this example. See the implementation of the :cpp:func:`lager::with_debugger` enhancer to see how to use perfect forwarding in this case. .. _perfect forwarding: https://en.cppreference.com/w/cpp/utility/forward Actors ------ In this section, we have seen how to combine and decorate models to create more complex applications. However, we are still using one single big central store. That is the normal mode of operation of Lager, which was designed to write interactive software using the unidirectional data-flow architecture. Most of the application is agnostic to the store anyways. Having a single store means we have a single place where mutation happens, which helps us avoid race conditions and eases testing and debugging the application. But there are indeed cases where it might be useful to have multiple stores. Since a store is associated to one event loop and a thread, you may want to use multiple stores to increase the parallelism of the app. The `Actors programming model`_ is a paradigm of concurrent computing that is based around independent entities, known as *actors*, that communicate with eachother via messages, as opposed to using shared memory. A :cpp:class:`lager::store` can be considered an Actor, to which you send messages using the :cpp:func:`dispatch()` method. A store can communicate with other actors using :ref:`effects` or :ref:`views`. Architecting your application around multiple actor stores that talk to eachother is a powerful tool to build distributed systems. You can use the store event loop interface to flexibly configure the level of parallelism for these intercommunicating entities. Furthermore, if you make your actions serializable, you can further scale up your application by having actors run seamlessly on different machines, sending eachother messages over network pipes. .. _actors programming model: https://en.wikipedia.org/wiki/Actor_model lager-0.1.1/doc/reducers.rst000066400000000000000000000122451451275761400157360ustar00rootroot00000000000000 .. _reducers: Reducers ======== Reducers are functions given a :ref:`model` and :ref:`action`, implements the update logic of the application, returning a new model the world. .. _purity: Purity ------ The reducer must be `referentially transparent`_ and a `pure function`_. This is a mouthful to say that, whenever you invoke the function with the same arguments, it produce the same result. This, in practice, means: - It must take all the arguments by value or const reference. - It must perform no side-effects, this is: no writing or reading to disk, generating non deterministic random numbers, making connections to servers, etc. - And of course, no reading or writing mutable globals, statics, singletons, and other evils. Sadly, C++ has no builtin mechanism to enforce function purity and this depends only on programmer discipline. It is technically possible to perform side effects directly from the reducer, but it is hard to say in general terms how much this messes up with the system. For programs that rely on the reproducibility of the reducer to implement *undo* or *time travel*, it can have fatal consequences. .. tip:: While the reducer must not *perform* side effects, it definitelly can, and often should, *describe* side effects. The :ref:`effects` section covers this aspect in detail. .. _pure function: https://en.wikipedia.org/wiki/Pure_function .. _referentially transparent: https://en.wikipedia.org/wiki/Referential_transparency Why the name? ------------- Given an ``action``, ``model`` types, and a ``update`` reducer function (in the :ref:`architecture` section we built a complete example), we can write a function that takes an initial model, a sequence of actions, and returns the current model after applying all the actions like this: .. code-block:: c++ model update_all(model init, const std::vector& actions) { return std::accumulate( actions.begin(), actions.end(), init, update); } The ``std::accumulate`` function is called ``reduce`` in many other languages, like Python, JavaScript, Scheme or Clojure. It takes a sequence of inputs and a binary operation, and it "reduces" the sequence to a single value, by succesively applying the binary operator to the last output and the next input. The **reducer** is the binary operation that we use to *reduce* a sequence of actions to a single model state---it is the last argument we passed to ``accumulate``. .. note:: *Reduce*, also known as *fold*, is one of the most important `higher-order functions`_ in functional programming, because it provides a general mechanism for performing iterative sequential computations. Transducers_ are a powerful abstraction based on the idea of *transforming reducers*. .. _std::accumulate: https://en.cppreference.com/w/cpp/algorithm/accumulate .. _transducers: https://www.youtube.com/watch?v=vohGJjGxtJQ .. _higher-order functions: https://en.wikipedia.org/wiki/Higher-order_function State machines -------------- As we depart from Object-Oriented Design and abandon UML class diagrams, we find ourselves devoid of modeling tools and visual representations for our system. `State machines`_ are a good tool to model Lager based systems, specially components with asynchronicity and transient states. We can follow the following analogies: .. _state machines: https://en.wikipedia.org/wiki/Finite-state_machine .. tabularcolumns:: |R|L| ====================== ====================== **model** *states* **actions** *transitions* **reducer** *transition table* ====================== ====================== .. image:: _static/torniquete.jpg :align: right :width: 12% Consider the example of a *turnstile*. A turnstile, used to control access to subways and amusement park rides, is a gate with three rotating arms at waist height, one across the entryway. Initially the arms are locked, blocking the entry, preventing patrons from passing through. Depositing a coin or token in a slot on the turnstile unlocks the arms, allowing a single customer to push through. After the customer passes through, the arms are locked again until another coin is inserted. This can be modelled by the following state diagram: (source_) .. _source: https://en.wikipedia.org/wiki/Finite-state_machine .. image:: _static/turnstile.svg :align: center :width: 70% Such diagram can be systematically translated into :ref:`model`, :ref:`action` and :ref:`reducers`, so that it can be executed in a Lager application: .. code-block:: c++ #include struct locked {}; struct unlocked {}; using model = std::variant; struct push {}; struct coin {}; using action = std::variant; model update(model m, action a) { return std::visit(lager::visitor{ [] (push) { return locked{}; }, [] (coin) { return unlocked{}; }, }, a); } In this case, the model was so simple that we only needed to pattern match the *action*. In more complicated cases we might need to analize the state inside the action (or otherwise) to fully implement the transition table. lager-0.1.1/doc/sphinx-html-hack.bash000077500000000000000000000060361451275761400174120ustar00rootroot00000000000000#!/bin/bash location=`dirname $0` echo "Running $0 at $location" # Fixes issues described here among others # https://github.com/michaeljones/breathe/issues/284 fix-missing-class-name() { src='\(.*\)class ' dst='\2class\1' sed -i "s@$src@$dst@g" $location/_build/html/*.html } fix-missing-struct-name() { src='\(.*\)struct ' dst='\2struct\1' sed -i "s@$src@$dst@g" $location/_build/html/*.html } fix-double-using-keyword() { src='usingusing ' dst='using ' sed -i "s@$src@$dst@g" $location/_build/html/*.html } fix-do-not-repeat-type-in-member-using-declaration() { src='using \(\([^:]*::\)*\)\([^ ]*\) = \([^<]*\)' dst='using \3 = \4' sed -i "s@$src@$dst@g" $location/_build/html/*.html } fix-do-not-repeat-type-in-member-using-declaration fix-remove-double-class-name() { # src='\([^&]*\)<\([^&]*\)>::' # dst='\1::' src='\([^<]*\)' dst='' sed -i "s@$src@$dst@g" $location/_build/html/*.html } fix-remove-straneous-typedefs() { src='typedef ' dst='' sed -i "s@$src@$dst@g" $location/_build/html/*.html } fix-remove-straneous-typedefs-2() { src='= typedef ' dst='= ' sed -i "s@$src@$dst@g" $location/_build/html/*.html } fix-remove-straneous-typedefs-2 fix-remove-straneous-using-declarations() { src='using template<>
' dst='' sed -i "s@$src@$dst@g" $location/_build/html/*.html } fix-remove-straneous-template-in-using-declarations-1() { src='\(
\n]*>\)\ntemplate<>
' dst='\1' pre=':a;N;$!ba;' sed -i "$pre;s@$src@$dst@g" $location/_build/html/*.html } fix-remove-straneous-template-in-using-declarations-1 fix-remove-straneous-template-in-using-declarations-2() { src='>template<>
`. The library uses these principles to provide a *time travelling debugger*. .. _screenshot: .. image:: _static/time-travel.png :width: 100% Time travelling debugger ------------------------ The time travelling debugger tool allows you to inspect the state and actions of a running Lager application. Furthermore, you can use it to bring the application to a previous state and continue running it from there. The screenshot_ above shows the debugger in action. At the bottom right corner we have a running Lager application, `an ncurses UI for our counter example`_. On the top right we have the UI of the time travelling debugger inspecting the application. On the right, we have a time-travelling debugger, debugging the former time-travelling debugger itself! .. _an ncurses ui for our counter example: https://github.com/arximboldi/lager/tree/master/example/counter/ncurses Enabling the debugger --------------------- You can enable the debugger by using the :cpp:func:`lager::with_debugger` :ref:`enhancer`. .. code-block:: c++ #include #include #include auto debugger = lager::http_debug_server{argc, argv, 8080}; auto store = lager::make_store<...>( ..., lager::with_debugger(debugger)); This enables the debugger, which can be accessed from http://localhost:8080 in a web browser. Since *enhancers* are compossable, you can instantiate a second debugger, that allows the inspection the state of the debugger itself: .. code-block:: c++ auto debugger = lager::http_debug_server{argc, argv, 8080}; auto meta_debugger = lager::http_debug_server{argc, argv, 8081}; auto store = lager::make_store<...>( ..., lager::with_debugger(debugger), lager::with_debugger(meta_debugger)); Debugger API ------------ The debugger also exposes an API that can be used to programatically inspect and query the application from the outside, via HTTP requests that return JSON data. It has the following endpoints. GET ``/api/`` Returns the current status of the application, including the number of existing steps and pause state. GET ``/api/step/{cursor}`` Query the action and resulting model at step number ``cursor``. POST ``/api/goto/{cursor}`` Bring the application to the state number ``cursor``. POST ``/api/undo`` Bring the application one step back. POST ``/api/redo`` Bring the application one step forward. POST ``/api/pause`` Pause the application (it its event loop supports it). POST ``/api/resume`` Resume a paused application (it its event loop supports it). Serialization ------------- For the debugger to work, the states and actions must be serializable to JSON using the Cereal_ library. Cereal itself already knows, out of the box, how to serialize most standard library value types and containers. Lager includes extensions supporting C++17 types like ``std::variant`` and ``std::optional``, as the Immer_ collections. .. _cereal: https://github.com/USCiLab/cereal .. _immer: https://sinusoid.es/immer/ For custom types you have to define the serialization yourself. This is however quite easy with the provided ``LAGER_CEREAL_STRUCT`` macro: .. code-block:: c++ #include struct model { int value; immer::box name; immer::vector times; }; LAGER_CEREAL_STRUCT(model, (value)(name)(times)); lager-0.1.1/doc/util.rst000066400000000000000000000003571451275761400151000ustar00rootroot00000000000000 util ==== Lager provides various little utilities that are convenient when using it. They mostly revolve around functional programming and variant visitation. ---- .. _noop: .. doxygengroup:: util :project: lager :content-only: lager-0.1.1/doc/views.rst000066400000000000000000000144461451275761400152640ustar00rootroot00000000000000 .. _views: Views ===== After the data-model is updated via a :ref:`Reducer`, all views connected with the :cpp:func:`lager::watch` function are called. There are various ways to implement a view in the Lager model. Value based UI -------------- In an ideal world, the *view* is just :ref:`a value` and the user interface is a function that takes a *model* and returns such *view*. This fits perfectly the unidirectional data-flow architecture. However, to do make this pattern convenient and efficient you need a supporting framework. In the JavaScript world, `virtual DOM`_ frameworks like React_ allow us to conveniently represent the UI as a value, and use this declarative description to efficiently manipulate an underlying :ref:`UI object tree`. Sadly, in the C++ world, no such library exists. This approach is therefore not practical for Lager applications at the moment. .. admonition:: Support the development of a value-based UI library for C++! :class: danger Thanks to value semantics and static polymorphism, C++ would indeed be a great language for a React-like library. We at `Sinusoidal Engineering`_ spend a lot of time thinking about how to further value-oriented design in C++, and have quite a bunch of ideas on how to build a declarative UI framework, one that does not reinvent the wheel and can be plugged on top of existing native frameworks like `Gtk+`_ or Qt_, Cocoa or the Windows API. This is however a non trivial project. *Help us save the planet: no more inefficient Electron apps!* Please consider sponsoring that project so that we can invest the time required to develop it, and also tailor it to your particular needs. `Contact us to make it happen!`_ .. _virtual dom: https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom .. _react: https://reactjs.org/ .. _sinusoidal engineering: http://sinusoid.al/ .. _qt: https://www.qt.io/ .. _gtk+: https://www.gtk.org .. _Contact us to make it happen!: mailto:juanpe@sinusoid.al Immediate mode UI ----------------- A practical alternative is the usage of `immediate mode UI`_. There are a few UI libraries following this approach and they are specially popular for the development of video games. These include ImGUI_ and FlatUI_. In an *immediate mode UI*, whenever you need to redraw, you traverse your whole state tree invoking functions of the framework as you go to represent it, drawing buttons, input boxes, labels, windows, etc. The architecture of a Lager application fits this model very well: our state is composed of simple value types that are easy and fast to traverse. Furthermore, using Lager helps solve some of the problems that immediate mode UI's have when scaling. One is *state tearing*. In a naive immediate mode UI, the state is mutated as you draw it, often directly by the framework primitives. This means that if the same piece of state is represented in multiple places, it might be inconsistent in a frame, as the mutation happens during the rendering. When using Lager with an immediate mode UI framework, you can not mutate the state directly, but :cpp:func:`dispatch` :ref:`actions` to trigger a state change cascade. Lager works in a transactional way, making all mutations traceable to the actions that triggered them, and ensuring that always consistent snapshots are rendered. .. admonition:: Exercise :class: tip Write an ImGUI interface for the todo-list app core core :ref:`we described before`. If you want it to be reviewed and potentially included as an offical Lager example, `make a pull request`_ to the project. .. _imgui: https://github.com/ocornut/imgui .. _flatui: http://google.github.io/flatui .. _immediate mode UI: https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics) .. _make a pull request: https://github.com/arximboldi/lager/pulls .. _object-ui: Widget tree UI -------------- Most C++ UI frameworks, like `Gtk+`_ or Qt_, are based on a widget tree represented by mutable objects, that notify events through listeners. These are designed around the assumption that your model is also a mutable object tree, that you can observe via listeners. Whenever the model is changed, a listener manipulates the widget tree to keep it updated. Whenever the user manipulates a widget, another listener manipulates the model. This is the kind of circular relationship that we wanted to avoid with the unidirectional data-flow. We can still use an unidirectional data-flow approach in combination with a widget tree UI, by breaking the circle as follows: - We connect listeners to the widgets as usual to observe user interactions, but they do not mutate the model directly. Instead, they *dispatch* actions. - There are no listeners connected to the model, that is indeed impossible, because the model is a value type. But the model can be easily copied, so we can *diff* it. We keep a copy of the last version around, such that when the new state of the application is pushed, we traverse the state, comparing the old and new values. When discrepancies are found, we update the widget tree in the same way we would do inside a listener. .. admonition:: Library support This diffing mechanism can be a bit cumbersome, and sometimes error prone. :ref:`cursors` can do it for you automatically. They can also do much more, and are an invaluable tool when `interfacing a value-oriented data model with an object-oriented UI`. Observables ----------- Another way to look at the state of a Lager application is as a *sequence of values over time*. Leveraging this realisation, we can apply the `reactive programming`_ paradigm to manipulate it. The `RxCpp`_ library is precisely designed to work with sequences of values that change over time. These sequences can be reified as values called *observables* that can be manipulated using higher order transformations and scheduling combinators. We can use the ``view`` function that is passed to the store to push these into an Rx observable. This is then used to feed other subsystems in a reactive manner. We can also use Rx observables to source the actions into the store. .. _reactive programming: http://reactivex.io/intro.html .. _RxCpp: https://github.com/ReactiveX/RxCpp lager-0.1.1/example/000077500000000000000000000000001451275761400142525ustar00rootroot00000000000000lager-0.1.1/example/CMakeLists.txt000066400000000000000000000146221451275761400170170ustar00rootroot00000000000000 add_custom_target(examples COMMENT "Build all examples") if (lager_BUILD_TESTS) add_dependencies(check examples) endif() # std examples # ==================== add_executable(counter-std EXCLUDE_FROM_ALL counter/std/main.cpp) target_link_libraries(counter-std lager-example) add_dependencies(examples counter-std) # ncurses examples # ================ set(CURSES_NEED_WIDE true) find_package(Curses) if(Curses_FOUND) if (lager_BUILD_DEBUGGER_EXAMPLES) set(counter_ncurses_sources counter/ncurses/main.cpp counter/ncurses/terminal.cpp) set(counter_ncurses_include_directories ${CURSES_INCLUDE_DIR}) set(counter_ncurses_link_libraries lager-debugger-example ${CURSES_LIBRARIES}) add_executable(counter-ncurses EXCLUDE_FROM_ALL ${counter_ncurses_sources}) target_include_directories(counter-ncurses SYSTEM PUBLIC ${counter_ncurses_include_directories}) target_link_libraries(counter-ncurses ${counter_ncurses_link_libraries}) target_compile_definitions(counter-ncurses PUBLIC DEBUGGER) add_dependencies(examples counter-ncurses) add_executable(counter-ncurses-tree EXCLUDE_FROM_ALL ${counter_ncurses_sources}) target_include_directories(counter-ncurses-tree SYSTEM PUBLIC ${counter_ncurses_include_directories}) target_link_libraries(counter-ncurses-tree ${counter_ncurses_link_libraries}) target_compile_definitions(counter-ncurses-tree PUBLIC TREE_DEBUGGER) add_dependencies(examples counter-ncurses-tree) add_executable(counter-ncurses-meta EXCLUDE_FROM_ALL ${counter_ncurses_sources}) target_include_directories(counter-ncurses-meta SYSTEM PUBLIC ${counter_ncurses_include_directories}) target_link_libraries(counter-ncurses-meta ${counter_ncurses_link_libraries}) target_compile_definitions(counter-ncurses-meta PUBLIC DEBUGGER META_DEBUGGER) add_dependencies(examples counter-ncurses-meta) endif() else() message(STATUS "Disabling Curses based examples") endif() # sdl examples # =========== find_package(SDL2) find_package(SDL2_ttf) if (SDL2_FOUND AND SDL2_ttf_FOUND) if(lager_BUILD_DEBUGGER_EXAMPLES) add_executable(counter-sdl2 EXCLUDE_FROM_ALL counter/sdl2/main.cpp) target_include_directories(counter-sdl2 SYSTEM PUBLIC ${SDL2_INCLUDE_DIRS} ${SDL2_TTF_INCLUDE_DIR}) target_link_libraries(counter-sdl2 lager-debugger-example ${SDL2_LIBRARIES} ${SDL2_TTF_LIBRARIES}) add_dependencies(examples counter-sdl2) add_executable(autopong EXCLUDE_FROM_ALL autopong/autopong.cpp autopong/sdl2/main.cpp) target_include_directories(autopong SYSTEM PUBLIC ${SDL2_INCLUDE_DIRS} ${SDL2_TTF_INCLUDE_DIR}) target_link_libraries(autopong lager-debugger-example ${SDL2_LIBRARIES} ${SDL2_TTF_LIBRARIES}) add_dependencies(examples autopong) add_executable(autopong-debug EXCLUDE_FROM_ALL autopong/autopong.cpp autopong/sdl2/main.cpp) target_compile_definitions(autopong-debug PUBLIC DEBUGGER) target_include_directories(autopong-debug SYSTEM PUBLIC ${SDL2_INCLUDE_DIRS} ${SDL2_TTF_INCLUDE_DIR}) target_link_libraries(autopong-debug lager-debugger-example ${SDL2_LIBRARIES} ${SDL2_TTF_LIBRARIES}) add_dependencies(examples autopong-debug) endif() else() message(STATUS "Disabling SDL based examples") endif() # qt examples # =========== find_package(Qt5Core) find_package(Qt5Concurrent) find_package(Qt5Qml) find_package(Qt5Gui) find_package(Qt5Widgets) find_package(Qt5QuickControls2) find_package(Boost 1.56) if (Qt5Core_FOUND AND Qt5Concurrent_FOUND AND Qt5Gui_FOUND AND Qt5Widgets_FOUND AND Qt5Qml_FOUND AND Qt5QuickControls2_FOUND AND Boost_FOUND) add_executable(todo-qml EXCLUDE_FROM_ALL todo/item.cpp todo/model.cpp todo/qml/main.cpp) set_target_properties(todo-qml PROPERTIES AUTOMOC YES) target_link_libraries(todo-qml lager-example Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Qml Qt5::QuickControls2) target_compile_definitions(todo-qml PRIVATE LAGER_TODO_QML_DIR="${CMAKE_CURRENT_SOURCE_DIR}/todo/qml") add_dependencies(examples todo-qml) add_executable(todo-qml-redux EXCLUDE_FROM_ALL todo/item.cpp todo/model.cpp todo/app.cpp todo/qml-redux/main.cpp) set_target_properties(todo-qml-redux PROPERTIES AUTOMOC YES) target_link_libraries(todo-qml-redux lager-example Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Qml Qt5::QuickControls2) target_compile_definitions(todo-qml-redux PRIVATE LAGER_TODO_QML_DIR="${CMAKE_CURRENT_SOURCE_DIR}/todo/qml-redux") add_dependencies(examples todo-qml-redux) add_executable(snake-qml EXCLUDE_FROM_ALL snake/model.cpp snake/qml/main.cpp snake/qml/qmodel.cpp) set_target_properties(snake-qml PROPERTIES AUTOMOC YES) target_link_libraries(snake-qml lager-example Qt5::Core Qt5::Concurrent Qt5::Gui Qt5::Widgets Qt5::Qml Qt5::QuickControls2) target_include_directories(snake-qml PRIVATE ${Boost_INCLUDE_DIRS}) target_compile_definitions(snake-qml PRIVATE LAGER_SNAKE_QML_DIR="${CMAKE_CURRENT_SOURCE_DIR}/snake/qml") add_dependencies(examples snake-qml) else() message(STATUS "Disabling Qt based examples") endif() # ImGui examples # ============== set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL) set(imgui_source_dir "$ENV{IMGUI_SOURCE_DIR}") set(imgui_sources "${imgui_source_dir}/imgui.cpp" "${imgui_source_dir}/imgui_draw.cpp" "${imgui_source_dir}/imgui_widgets.cpp" "${imgui_source_dir}/imgui_impl_sdl.cpp" "${imgui_source_dir}/imgui_impl_opengl3.cpp") find_program(EMXX em++) if (EMXX AND OPENGL_FOUND AND SDL2_FOUND AND EXISTS "${imgui_source_dir}/imgui.h") message(STATUS "ImGui found in ${imgui_source_dir}") set(extra_emcc_flags $ENV{NIX_CFLAGS_COMPILE}) separate_arguments(extra_emcc_flags) add_custom_target(todo-imgui COMMAND ${EMXX} ${extra_emcc_flags} -Os -std=c++17 -I ${CMAKE_SOURCE_DIR} -I ${imgui_source_dir} ${imgui_sources} ${CMAKE_CURRENT_SOURCE_DIR}/todo/item.cpp ${CMAKE_CURRENT_SOURCE_DIR}/todo/model.cpp ${CMAKE_CURRENT_SOURCE_DIR}/todo/imgui/main.cpp -s USE_SDL=2 -s USE_WEBGL2=1 -s WASM=1 -s FULL_ES3=1 -s ALLOW_MEMORY_GROWTH=1 --emrun --shell-file ${CMAKE_SOURCE_DIR}/resources/emscripten_shell_minimal.html -o ${CMAKE_CURRENT_BINARY_DIR}/todo-imgui.html COMMENT "Build todo-imgui emscripten example") add_dependencies(examples todo-imgui) else() message(STATUS "Disabling ImGui based examples") endif() lager-0.1.1/example/autopong/000077500000000000000000000000001451275761400161065ustar00rootroot00000000000000lager-0.1.1/example/autopong/autopong.cpp000066400000000000000000000100411451275761400204420ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "autopong.hpp" namespace autopong { namespace { float segment_squared_distance(point l1p1, point l1p2, point l2p1, point l2p2) { constexpr auto epsilon = 0.00000001; auto u = l1p2 - l1p1; auto v = l2p2 - l2p1; auto w = l1p1 - l2p1; auto a = dot(u, u), b = dot(u, v), c = dot(v, v), d = dot(u, w), e = dot(v, w); auto D = a * c - b * b; float sc, sN, sD = D; float tc, tN, tD = D; if (D < epsilon) { sN = 0.0; sD = 1.0; tN = e; tD = c; } else { sN = (b * e - c * d); tN = (a * e - b * d); if (sN < 0.0) { sN = 0.0; tN = e; tD = c; } else if (sN > sD) { sN = sD; tN = e + b; tD = c; } } if (tN < 0.0) { tN = 0.0; if (-d < 0.0) sN = 0.0; else if (-d > a) sN = sD; else { sN = -d; sD = a; } } else if (tN > tD) { tN = tD; if ((-d + b) < 0.0) sN = 0; else if ((-d + b) > a) sN = sD; else { sN = (-d + b); sD = a; } } sc = std::abs(sN) < epsilon ? 0.0 : sN / sD; tc = std::abs(tN) < epsilon ? 0.0 : tN / tD; auto dP = w + (u * sc) - (v * tc); return norm(dP); } } // namespace model update(model g, action a) { return std::visit( lager::visitor{ [&](paddle_move_action a) { g.paddle_x = std::max(0.f, std::min((float) window_width - paddle_width, g.paddle_x + a.delta * paddle_sens)); return g; }, [&](tick_action a) { auto ball = g.ball + g.ball_v * a.delta; g.death_anim = std::max(0.f, g.death_anim - a.delta * death_anim_speed); g.bounce_anim = std::max(0.f, g.bounce_anim - a.delta * bounce_anim_speed); if ((x(g.ball_v) < 0 && x(ball) - ball_r <= padding) || (x(g.ball_v) > 0 && x(ball) + ball_r >= window_width - padding)) x(g.ball_v, -x(g.ball_v)); if (y(g.ball_v) < 0 && y(ball) - ball_r <= padding) y(g.ball_v, -y(g.ball_v)); if (y(g.ball_v) > 0 && ball_r * ball_r > segment_squared_distance( g.ball, ball, point{g.paddle_x - ball_r, paddle_y}, point{g.paddle_x + paddle_width + ball_r, paddle_y})) { y(g.ball_v, -y(g.ball_v)); g.ball_v *= ball_a; g.score++; g.bounce_anim = 1; } else if (y(g.ball_v) > 0 && y(ball) - ball_r >= window_height - padding) { g.max_score = std::max(g.max_score, g.score); g.score = 0; g.ball_v = ball_init_v; g.ball = {padding + std::rand() / static_cast(RAND_MAX) * (window_width - padding * 4), padding * 2}; g.death_anim = 1; } else g.ball = ball; return g; }}, a); } } // namespace autopong lager-0.1.1/example/autopong/autopong.hpp000066400000000000000000000042201451275761400204510ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include #include #include #include namespace autopong { using point = std::complex; inline float x(const point& p) { return p.real(); } inline void x(point& p, float v) { return p.real(v); } inline float y(const point& p) { return p.imag(); } inline void y(point& p, float v) { return p.imag(v); } inline float dot(const point& a, const point& b) { return x(conj(a) * b); } constexpr auto window_width = 800; constexpr auto window_height = 600; constexpr auto padding = 20; constexpr auto border = 4; constexpr auto ball_r = 4; constexpr auto ball_init_v = point{0.2f, 0.2f}; constexpr auto ball_a = 1.1f; constexpr auto paddle_width = 100; constexpr auto paddle_height = 10; constexpr auto paddle_y = window_height - 2 * padding - paddle_height; constexpr auto paddle_sens = 0.5f; constexpr auto bounce_anim_speed = 0.002f; constexpr auto death_anim_speed = 0.001f; struct model { int score = 0; int max_score = 0; point ball = {window_width / 2, padding * 2}; point ball_v = ball_init_v; float paddle_x = window_width / 2 - paddle_width / 2; float death_anim = 0; float bounce_anim = 0; }; struct paddle_move_action { float delta; }; struct tick_action { float delta; }; using action = std::variant; model update(model g, action a); } // namespace autopong LAGER_STRUCT(autopong, model, score, max_score, ball, ball_v, paddle_x, death_anim, bounce_anim); LAGER_STRUCT(autopong, paddle_move_action, delta); LAGER_STRUCT(autopong, tick_action, delta); lager-0.1.1/example/autopong/sdl2/000077500000000000000000000000001451275761400167525ustar00rootroot00000000000000lager-0.1.1/example/autopong/sdl2/main.cpp000066400000000000000000000133721451275761400204100ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "../autopong.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include constexpr auto font_size = 32; std::string font_path() { using namespace std::string_literals; return lager::resources_path() + "/SourceSansPro-Bold.ttf"s; } struct sdl_view { SDL_Window* window = SDL_CreateWindow("Lager Counter Example", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, autopong::window_width, autopong::window_height, SDL_WINDOW_SHOWN); SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, 0); TTF_Font* font = TTF_OpenFont(font_path().c_str(), font_size); }; void draw(sdl_view& v, autopong::model g) { using namespace autopong; SDL_RenderClear(v.renderer); // render background { SDL_SetRenderDrawColor(v.renderer, 0, 0, 0, 255); auto rect = SDL_Rect{0, 0, window_width, window_height}; SDL_RenderFillRect(v.renderer, &rect); } // render text { auto c = (std::uint8_t)(255 * (1 - std::cos(g.death_anim * M_PI - M_PI / 2))); auto msg = std::to_string(g.score); auto surf = TTF_RenderText_Blended(v.font, msg.c_str(), {255, c, c, 255}); auto text = SDL_CreateTextureFromSurface(v.renderer, surf); SDL_FreeSurface(surf); auto rect = SDL_Rect{2 * padding, 2 * padding - font_size / 3}; SDL_QueryTexture(text, nullptr, nullptr, &rect.w, &rect.h); SDL_RenderCopy(v.renderer, text, nullptr, &rect); if (g.max_score) { auto msg = std::to_string(g.max_score); auto surf = TTF_RenderText_Blended(v.font, msg.c_str(), {255, c, c, 255}); auto text = SDL_CreateTextureFromSurface(v.renderer, surf); SDL_FreeSurface(surf); auto rect = SDL_Rect{2 * padding, 2 * padding - font_size / 3}; SDL_QueryTexture(text, nullptr, nullptr, &rect.w, &rect.h); rect.x = window_width - rect.x - rect.w; SDL_RenderCopy(v.renderer, text, nullptr, &rect); } } // render border { auto c = (std::uint8_t)(255 * (1 - std::cos(g.death_anim * M_PI - M_PI / 2))); SDL_SetRenderDrawColor(v.renderer, 255, c, c, 255); auto rect = SDL_Rect{padding - border, padding - border, window_width - 2 * padding + 2 * border, window_height - 2 * padding + 2 * border}; for (auto i = 0; i < border; ++i, ++rect.x, ++rect.y, ----rect.w, ----rect.h) SDL_RenderDrawRect(v.renderer, &rect); } // render game { auto rect = SDL_Rect{ padding, padding, window_width - 2 * padding, window_height - 2 * padding, }; auto c = (std::uint8_t)(255 * (1. - std::sin(g.bounce_anim * M_PI / 2))); SDL_RenderSetClipRect(v.renderer, &rect); SDL_SetRenderDrawColor(v.renderer, 255, c, c, 255); // paddle { auto rect = SDL_Rect{ (int) g.paddle_x, paddle_y, paddle_width, paddle_height}; SDL_RenderFillRect(v.renderer, &rect); } // ball { auto rect = SDL_Rect{(int) x(g.ball) - ball_r, (int) y(g.ball) - ball_r, ball_r * 2, ball_r * 2}; SDL_RenderFillRect(v.renderer, &rect); } SDL_RenderSetClipRect(v.renderer, nullptr); } SDL_RenderPresent(v.renderer); } std::optional intent(const SDL_Event& event) { switch (event.type) { case SDL_MOUSEMOTION: return autopong::paddle_move_action{(float) event.motion.xrel}; default: return std::nullopt; } } int main(int argc, const char** argv) { using namespace std::placeholders; SDL_Init(SDL_INIT_VIDEO); TTF_Init(); SDL_SetRelativeMouseMode(SDL_TRUE); #ifdef DEBUGGER auto debugger = lager::http_debug_server{argc, argv, 8080, lager::resources_path()}; #endif auto view = sdl_view{}; auto loop = lager::sdl_event_loop{}; auto store = lager::make_store(autopong::model{}, lager::with_sdl_event_loop{loop}, #ifdef DEBUGGER lager::with_debugger(debugger) #else lager::identity #endif ); watch(store, [&](auto&& val) { draw(view, LAGER_FWD(val)); }); loop.run( [&](const SDL_Event& ev) { if (auto act = intent(ev)) store.dispatch(*act); return ev.type != SDL_QUIT; }, [&](float delta) { store.dispatch(autopong::tick_action{delta}); return true; }); } lager-0.1.1/example/counter/000077500000000000000000000000001451275761400157315ustar00rootroot00000000000000lager-0.1.1/example/counter/counter.hpp000066400000000000000000000024421451275761400201230ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include #include #include namespace counter { struct model { int value = 0; }; struct increment_action {}; struct decrement_action {}; struct reset_action { int new_value = 0; }; using action = std::variant; inline model update(model c, action action) { return std::visit(lager::visitor{ [&](increment_action) { return model{c.value + 1}; }, [&](decrement_action) { return model{c.value - 1}; }, [&](reset_action a) { return model{a.new_value}; }, }, action); } } // namespace counter LAGER_STRUCT(counter, model, value); LAGER_STRUCT(counter, increment_action); LAGER_STRUCT(counter, decrement_action); LAGER_STRUCT(counter, reset_action, new_value); lager-0.1.1/example/counter/ncurses/000077500000000000000000000000001451275761400174135ustar00rootroot00000000000000lager-0.1.1/example/counter/ncurses/main.cpp000066400000000000000000000075031451275761400210500ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "../counter.hpp" #include "terminal.hpp" #include #include #include #include #include #include #include #include using namespace std::string_literals; void draw(const counter::model& c) { static const auto prelude = "current value is "s; static const auto instructions = " arrow up -- decrease counter\n" " arrow down -- increase counter\n" " space bar -- reset counter"; auto message = prelude + std::to_string(c.value); auto max_x = 0, max_y = 0; getmaxyx(stdscr, max_y, max_x); const auto border_y = 2, border_x = 4; auto width = message.size(); auto pos_y = max_y / 2; auto pos_x = (max_x - message.size()) / 2; ::clear(); auto color = c.value < -3 ? 3 : c.value > 3 ? 2 /* else */ : 1; ::attron(COLOR_PAIR(color)); ::bkgd(COLOR_PAIR(color)); attrset(A_NORMAL); mvprintw(max_y - 4, 0, instructions); attrset(A_NORMAL | A_REVERSE | A_BOLD); for (auto y = pos_y - border_y; y <= pos_y + border_y; ++y) mvhline(y, pos_x - border_x, ' ', width + 2 * border_x); mvaddnstr(pos_y, pos_x, message.data(), width); ::curs_set(false); ::refresh(); } int main(int argc, const char** argv) { auto serv = boost::asio::io_service{}; auto term = ncurses::terminal{serv}; ::init_pair(1, COLOR_WHITE, COLOR_GREEN); ::init_pair(2, COLOR_WHITE, COLOR_RED); ::init_pair(3, COLOR_WHITE, COLOR_BLUE); #if defined(DEBUGGER) || defined(TREE_DEBUGGER) auto debugger = lager::http_debug_server{argc, argv, 8080, lager::resources_path()}; #endif #ifdef META_DEBUGGER auto meta_debugger = lager::http_debug_server{argc, argv, 8081, lager::resources_path()}; #endif auto store = lager::make_store( counter::model{}, lager::with_boost_asio_event_loop{serv.get_executor()}, zug::comp( #ifdef DEBUGGER lager::with_debugger(debugger), #endif #ifdef TREE_DEBUGGER lager::with_debugger(debugger), #endif #ifdef META_DEBUGGER lager::with_debugger(meta_debugger), #endif lager::identity)); watch(store, [](auto&& val) { draw(unwrap(val)); }); draw(unwrap(store.get())); term.start([&](auto ev) { std::visit( lager::visitor{ [&](ncurses::key_event ev) { if (ev.key == ncurses::key_code{KEY_CODE_YES, KEY_UP}) store.dispatch(counter::increment_action{}); else if (ev.key == ncurses::key_code{KEY_CODE_YES, KEY_DOWN}) store.dispatch(counter::decrement_action{}); else if (ev.key == ncurses::key_code{OK, ' '}) store.dispatch(counter::reset_action{}); else if (ev.key == ncurses::key_code{OK, 'q'} || ev.key == ncurses::key_code{OK, '[' - '@'}) // esc term.stop(); }, [&](ncurses::resize_event) { draw(unwrap(store.get())); }}, ev); }); serv.run(); } lager-0.1.1/example/counter/ncurses/terminal.cpp000066400000000000000000000051031451275761400217310ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "terminal.hpp" #include #include using namespace std::placeholders; using namespace std::string_literals; namespace ncurses { terminal::terminal(boost::asio::io_service& serv) : win_{[] { std::locale::global(std::locale("")); ::setlocale(LC_ALL, ""); return ::initscr(); }()} , input_{serv, ::dup(STDIN_FILENO)} , signal_{serv, SIGWINCH} { if (win_.get() != ::stdscr) throw std::runtime_error{"error while initializing ncurses"}; ::raw(); ::noecho(); ::keypad(stdscr, true); ::nodelay(stdscr, true); ::start_color(); ::use_default_colors(); } coord terminal::size() { int maxrow, maxcol; getmaxyx(stdscr, maxrow, maxcol); return {maxrow, maxcol}; } void terminal::start(event_handler ev) { assert(!handler_); handler_ = std::move(ev); next_key_(); next_resize_(); } void terminal::stop() { input_.cancel(); signal_.cancel(); handler_ = {}; } void terminal::next_resize_() { signal_.async_wait([this](auto ec, auto) { if (!ec) { next_resize_(); auto ws = ::winsize{}; if (::ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) ::perror("TIOCGWINSZ"); else { ::resizeterm(ws.ws_row, ws.ws_col); handler_(resize_event{{ws.ws_row, ws.ws_col}}); } } }); } void terminal::next_key_() { using namespace boost::asio; input_.async_read_some(null_buffers(), [&](auto ec, auto) { if (!ec) { auto key = wint_t{}; auto res = int{}; while (ERR != (res = ::wget_wch(win_.get(), &key))) { next_key_(); handler_(key_event{{res, key}}); } } }); } void terminal::cleanup_fn::operator()(WINDOW* win) const { if (win) { // consume all remaining characters from the terminal so they // don't leak in the bash prompt after quitting, then restore // the terminal state auto key = wint_t{}; while (::get_wch(&key) != ERR) ; ::endwin(); } } } // namespace ncurses lager-0.1.1/example/counter/ncurses/terminal.hpp000066400000000000000000000032371451275761400217440ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include #include #include #include #include extern "C" { // this is required to avoid naming conflicts with boost::asio #ifndef NCURSES_NOMACROS #define NCURSES_NOMACROS 1 #endif // this is required on Apple for ::get_wch #ifndef _XOPEN_SOURCE_EXTENDED #define _XOPEN_SOURCE_EXTENDED 1 #endif #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE 1 #endif #include } namespace ncurses { struct coord { int row = {}; int col = {}; }; using key_code = std::tuple; struct key_event { key_code key; }; struct resize_event { coord size; }; using event = std::variant; struct terminal { using event_handler = std::function; terminal(boost::asio::io_service& serv); coord size(); void start(event_handler ev); void stop(); private: struct cleanup_fn { void operator()(_win_st* win) const; }; void next_key_(); void next_resize_(); std::unique_ptr<_win_st, cleanup_fn> win_; boost::asio::posix::stream_descriptor input_; boost::asio::signal_set signal_; event_handler handler_; }; } // namespace ncurses lager-0.1.1/example/counter/sdl2/000077500000000000000000000000001451275761400165755ustar00rootroot00000000000000lager-0.1.1/example/counter/sdl2/main.cpp000066400000000000000000000061331451275761400202300ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include #include #include "../counter.hpp" #include #include #include #include #include std::string font_path() { using namespace std::string_literals; return lager::resources_path() + "/SourceSansPro-Regular.ttf"s; } struct sdl_view { int width = 640; int height = 480; SDL_Window* window = SDL_CreateWindow("Lager Counter Example", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, SDL_WINDOW_SHOWN); SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, 0); TTF_Font* font = TTF_OpenFont(font_path().c_str(), 24); }; void draw(sdl_view& v, counter::model c) { SDL_RenderClear(v.renderer); // render background { SDL_SetRenderDrawColor(v.renderer, 255, 96, 128, 255); auto rect = SDL_Rect{0, 0, v.width, v.height}; SDL_RenderFillRect(v.renderer, &rect); } // render text { auto msg = "counter value is " + std::to_string(c.value); auto surf = TTF_RenderText_Blended(v.font, msg.c_str(), {255, 255, 255, 255}); auto text = SDL_CreateTextureFromSurface(v.renderer, surf); SDL_FreeSurface(surf); auto rect = SDL_Rect{}; SDL_QueryTexture(text, nullptr, nullptr, &rect.w, &rect.h); rect.x = v.width / 2 - rect.w / 2; rect.y = v.height / 2 - rect.h / 2; SDL_RenderCopy(v.renderer, text, nullptr, &rect); } SDL_RenderPresent(v.renderer); } std::optional intent(const SDL_Event& event) { if (event.type == SDL_KEYDOWN) { switch (event.key.keysym.sym) { case SDLK_UP: return counter::increment_action{}; case SDLK_DOWN: return counter::decrement_action{}; case SDLK_SPACE: return counter::reset_action{}; default: break; } } return std::nullopt; } int main() { using namespace std::placeholders; SDL_Init(SDL_INIT_VIDEO); TTF_Init(); auto view = sdl_view{}; auto loop = lager::sdl_event_loop{}; auto store = lager::make_store( counter::model{}, lager::with_sdl_event_loop{loop}); watch(store, [&](auto&& val) { draw(view, val); }); draw(view, store.get()); loop.run([&](const SDL_Event& ev) { if (auto act = intent(ev)) store.dispatch(*act); return ev.type != SDL_QUIT; }); } lager-0.1.1/example/counter/std/000077500000000000000000000000001451275761400165235ustar00rootroot00000000000000lager-0.1.1/example/counter/std/main.cpp000066400000000000000000000022661451275761400201610ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "../counter.hpp" #include #include #include void draw(counter::model curr) { std::cout << "current value: " << curr.value << '\n'; } std::optional intent(char event) { switch (event) { case '+': return counter::increment_action{}; case '-': return counter::decrement_action{}; case '.': return counter::reset_action{}; default: return std::nullopt; } } int main() { auto store = lager::make_store( counter::model{}, lager::with_manual_event_loop{}); watch(store, draw); auto event = char{}; while (std::cin >> event) { if (auto act = intent(event)) store.dispatch(*act); } } lager-0.1.1/example/snake/000077500000000000000000000000001451275761400153535ustar00rootroot00000000000000lager-0.1.1/example/snake/model.cpp000066400000000000000000000076111451275761400171640ustar00rootroot00000000000000#include "model.hpp" #include #include #include #include namespace sn { namespace { template T wrap(T val, T min, T max) { if (val < min) return max - (min - val) + 1; if (val > max) return min + (val - max) - 1; return val; } direction left(direction initial) { if (initial == direction::up || initial == direction::down) return direction::left; return initial; } direction right(direction initial) { if (initial == direction::up || initial == direction::down) return direction::right; return initial; } direction up(direction initial) { if (initial == direction::left || initial == direction::right) return direction::up; return initial; } direction down(direction initial) { if (initial == direction::left || initial == direction::right) return direction::down; return initial; } point_t move_forward(point_t pos, direction dir) { switch (dir) { case direction::left: return {x(pos) - 1, y(pos)}; case direction::up: return {x(pos), y(pos) - 1}; case direction::right: return {x(pos) + 1, y(pos)}; case direction::down: return {x(pos), y(pos) + 1}; } BOOST_UNREACHABLE_RETURN(pos); } snake_model::body_t move_forward(snake_model::body_t body, direction dir) { const auto head = move_forward(body.front(), dir); std::rotate(body.begin(), body.end() - 1, body.end()); body[0] = head; return body; } bool in_bounds(point_t p) { return x(p) >= 0 && x(p) < game_model::width && y(p) >= 0 && y(p) < game_model::height; } template game_model make_game(Func&& random) { const point_t snake_head{game_model::width / 2, game_model::height / 2}; snake_model::body_t body{snake_head, {x(snake_head) - 1, y(snake_head)}, {x(snake_head) - 2, y(snake_head)}}; return {snake_model{body, direction::right}, random_apple_pos(std::forward(random)), false}; } } // namespace app_model make_initial(int seed) { std::mt19937 gen{unsigned(seed)}; std::uniform_int_distribution<> dist{0, game_model::width - 1}; return {gen, dist, make_game([&] { return dist(gen); })}; } app_model update(app_model m, action_t action) { return boost::apply_visitor( lager::visitor{ [&](go_left) { m.game.snake.dir = left(m.game.snake.dir); return m; }, [&](go_right) { m.game.snake.dir = right(m.game.snake.dir); return m; }, [&](go_up) { m.game.snake.dir = up(m.game.snake.dir); return m; }, [&](go_down) { m.game.snake.dir = down(m.game.snake.dir); return m; }, [&](tick) { const auto prev_back = m.game.snake.body.back(); auto body = move_forward(m.game.snake.body, m.game.snake.dir); const auto head = body.front(); if (std::any_of(body.begin() + 1, body.end(), [&](const auto& p) { return p == head; }) || !in_bounds(head)) { m.game.over = true; return m; } if (prev_back == m.game.apple_pos) { body.push_back(prev_back); m.game.apple_pos = random_apple_pos([&] { return m.dist(m.gen); }); } m.game.snake.body = body; return m; }, [&](reset) { m.game = make_game([&] { return m.dist(m.gen); }); return m; }}, action); } } // namespace snlager-0.1.1/example/snake/model.hpp000066400000000000000000000027561451275761400171760ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2019 Carl Bussey // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include #include #include #include #include namespace sn { struct go_left {}; struct go_right {}; struct go_up {}; struct go_down {}; struct reset {}; struct tick {}; using action_t = boost::variant; using point_t = std::pair; inline int x(point_t p) { return p.first; } inline int y(point_t p) { return p.second; } enum class direction { left, up, right, down }; struct snake_model { using body_t = std::vector; body_t body{}; direction dir{}; }; struct game_model { snake_model snake; point_t apple_pos{}; bool over; static constexpr int width{25}; static constexpr int height{25}; }; struct app_model { std::mt19937 gen; std::uniform_int_distribution<> dist; game_model game; }; template inline point_t random_apple_pos(Func&& random) { return {random(), random()}; } app_model make_initial(int seed); app_model update(app_model m, action_t action); } // namespace sn lager-0.1.1/example/snake/qml/000077500000000000000000000000001451275761400161445ustar00rootroot00000000000000lager-0.1.1/example/snake/qml/main.cpp000066400000000000000000000025111451275761400175730ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2019 Carl Bussey // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "../model.hpp" #include "qmodel.hpp" #include #include #include #include #include #include #include using namespace sn; int main(int argc, char** argv) { QApplication app{argc, argv}; QQmlApplicationEngine engine; std::random_device rd; auto initial_state = make_initial(rd()); auto store = lager::make_store(std::move(initial_state), lager::with_qt_event_loop{app}); Game game{store}; watch(store, [&](auto&& state) { game.setModel(state.game); }); game.setModel(store.get().game); auto* qmlContext = engine.rootContext(); qmlContext->setContextProperty(QStringLiteral("game"), &game); qRegisterMetaType("SnakeBody*"); engine.load(LAGER_SNAKE_QML_DIR "/main.qml"); return app.exec(); } lager-0.1.1/example/snake/qml/main.qml000066400000000000000000000054071451275761400176110ustar00rootroot00000000000000import QtQml 2.11 import QtQuick 2.6 import QtQuick.Controls 2.4 import QtQuick.Dialogs 1.3 ApplicationWindow { id: window width: 500 height: 500 visible: true property int modelScale: width / game.width readonly property int initial_timer_period: 100 readonly property int timer_decrement: 10 readonly property int min_timer_period: 10 readonly property int timer_change_points: 5 readonly property int initial_snake_size: 3 Timer { interval: Math.max(initial_timer_period - timer_decrement * Math.floor((snake.count - initial_snake_size) / timer_change_points) , min_timer_period) repeat: true running: !game.over onTriggered: { game.tick() } } MessageDialog { id: overDialog text: qsTr("Game Over") onAccepted: { game.reset() } } Connections { target: game onOverChanged: { if (game.over) overDialog.open() } } Connections { target: game onApplePositionChanged: { apple.state == "other" ? apple.state = "" : apple.state = "other" } } Item { anchors.fill: parent Rectangle { id: background color: "black" anchors.fill: parent } focus: true Keys.onPressed: { if (event.key == Qt.Key_Left) { game.left() event.accepted = true } else if (event.key == Qt.Key_Right) { game.right() event.accepted = true } else if (event.key == Qt.Key_Up) { game.up() event.accepted = true } else if (event.key == Qt.Key_Down) { game.down() event.accepted = true } } Rectangle { id: apple width: modelScale height: modelScale x: game.applePosition.x * modelScale y: game.applePosition.y * modelScale color: "red" states: [ State { name: "other" changes: [ PropertyChanges { target: apple; color: "green" } ] } ] } Repeater { id: snake model: game.snake property color color: "dark green" delegate: Rectangle { width: modelScale height: modelScale x: model.display.x * modelScale y: model.display.y * modelScale color: snake.color } } } } lager-0.1.1/example/snake/qml/qmodel.cpp000066400000000000000000000036321451275761400201350ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2019 Carl Bussey // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "qmodel.hpp" namespace sn { namespace { QPoint as_qpoint(std::pair p) { auto [x, y] = p; return {x, y}; } } // namespace SnakeBody::SnakeBody(QObject* parent) : QAbstractListModel(parent) {} void SnakeBody::setModel(snake_model::body_t body) { if (data_ != body) { beginResetModel(); data_ = body; endResetModel(); } } int SnakeBody::rowCount(const QModelIndex&) const { return data_.size(); } QVariant SnakeBody::data(const QModelIndex& index, int) const { if (index.row() < static_cast(data_.size())) return as_qpoint(data_[index.row()]); return {}; } Game::Game(context_t context) : context_{std::move(context)} , snakeBody_{new SnakeBody(this)} {} void Game::setModel(game_model game) { snakeBody_->setModel(game.snake.body); auto newApplePosition = as_qpoint(game.apple_pos); if (applePosition_ != newApplePosition) { applePosition_ = newApplePosition; emit applePositionChanged(applePosition_); } if (over_ != game.over) { over_ = game.over; emit overChanged(); } } Q_INVOKABLE void Game::left() { context_.dispatch(go_left{}); } Q_INVOKABLE void Game::right() { context_.dispatch(go_right{}); } Q_INVOKABLE void Game::up() { context_.dispatch(go_up{}); } Q_INVOKABLE void Game::down() { context_.dispatch(go_down{}); } Q_INVOKABLE void Game::reset() { context_.dispatch(sn::reset{}); } Q_INVOKABLE void Game::tick() { context_.dispatch(sn::tick{}); } } // namespace sn lager-0.1.1/example/snake/qml/qmodel.hpp000066400000000000000000000035421451275761400201420ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2019 Carl Bussey // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include "../model.hpp" #include #include #include #include namespace sn { class SnakeBody : public QAbstractListModel { Q_OBJECT public: SnakeBody(QObject* parent); void setModel(snake_model::body_t body); int rowCount(const QModelIndex& = QModelIndex()) const override; QVariant data(const QModelIndex& index, int = Qt::DisplayRole) const override; private: snake_model::body_t data_; }; class Game : public QObject { Q_OBJECT public: using context_t = lager::context; Game(context_t context); Q_PROPERTY(int width MEMBER width_ CONSTANT) Q_PROPERTY(int height MEMBER height_ CONSTANT) Q_PROPERTY( QPoint applePosition MEMBER applePosition_ NOTIFY applePositionChanged) Q_PROPERTY(bool over MEMBER over_ NOTIFY overChanged) Q_PROPERTY(SnakeBody* snake MEMBER snakeBody_ CONSTANT) Q_INVOKABLE void left(); Q_INVOKABLE void right(); Q_INVOKABLE void up(); Q_INVOKABLE void down(); Q_INVOKABLE void reset(); Q_INVOKABLE void tick(); void setModel(game_model game); signals: void applePositionChanged(QPoint); void overChanged(); private: context_t context_; SnakeBody* snakeBody_; static constexpr int width_{game_model::width}; static constexpr int height_{game_model::height}; QPoint applePosition_; bool over_{false}; }; } // namespace sn lager-0.1.1/example/todo/000077500000000000000000000000001451275761400152175ustar00rootroot00000000000000lager-0.1.1/example/todo/app.cpp000066400000000000000000000043411451275761400165050ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "app.hpp" #include namespace todo { app_result update(app s, app_action a) { return lager::match(std::move(a))( [&](save_action&& a) -> app_result { s.path = a.file.replace_extension("todo"); auto eff = [m = s.doc, f = s.path](auto&& ctx) { try { std::cout << "saving file: " << f << std::endl; save(f, m); } catch (std::exception const& err) { std::cerr << "error saving file: " << err.what() << std::endl; lager::get(ctx).error("Could not save file: " + f.string()); } }; return {std::move(s), eff}; }, [&](load_action&& a) -> app_result { auto eff = [f = std::move(a.file)](auto&& ctx) { std::cout << "loading file: " << f << std::endl; try { auto m = load(f); ctx.dispatch(load_result_action{f, std::move(m)}); } catch (std::exception const& err) { std::cerr << "error loading file: " << err.what() << std::endl; lager::get(ctx).error("Could not load file: " + f.string()); } }; return {std::move(s), eff}; }, [&](load_result_action&& a) -> app_result { s.doc = std::move(a.doc); s.path = std::move(a.file); return std::move(s); }, [&](model_action&& a) -> app_result { s.doc = update(std::move(s.doc), std::move(a)); return std::move(s); }); } } // namespace todo lager-0.1.1/example/todo/app.hpp000066400000000000000000000021101451275761400165020ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include "model.hpp" #include namespace todo { struct app { model doc; std::filesystem::path path; }; struct save_action { std::filesystem::path file; }; struct load_action { std::filesystem::path file; }; struct load_result_action { std::filesystem::path file; model doc; }; using app_action = std::variant; struct logger { std::function error; }; using app_result = lager::result>; app_result update(app, app_action); } // namespace todo LAGER_STRUCT(todo, app, doc, path); lager-0.1.1/example/todo/imgui/000077500000000000000000000000001451275761400163315ustar00rootroot00000000000000lager-0.1.1/example/todo/imgui/main.cpp000066400000000000000000000135031451275761400177630ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "../model.hpp" #include #include #include #include #include #include #include #include constexpr int window_padding = 48; constexpr int window_width = 800; constexpr int window_height = 600; // Sadly, ImGui sometimes forces us to store transient state, like text inputs. // We use this store this. struct ui_state { static constexpr std::size_t input_string_size = 1 << 10; std::array new_todo_input{'\0'}; }; void draw(lager::context ctx, const todo::item& i) { auto checked = i.done; if (ImGui::Checkbox("", &checked)) { ctx.dispatch(todo::toggle_item_action{}); } ImGui::SameLine(); ImGui::Text("%s", i.text.c_str()); ImGui::SameLine(); if (ImGui::Button("Delete")) { ctx.dispatch(todo::remove_item_action{}); } } void draw(lager::context ctx, const todo::model& m, ui_state& s) { ImGui::SetNextWindowPos({window_padding, window_padding}, ImGuiCond_Once); ImGui::SetNextWindowSize( {window_width - 2 * window_padding, window_height - 2 * window_padding}, ImGuiCond_Once); ImGui::Begin("Todo app"); if (ImGui::BeginPopup("not-implemented")) { ImGui::Text("Saving and loading have not been implemented!"); ImGui::EndPopup(); } if (ImGui::Button("Save")) ImGui::OpenPopup("not-implemented"); ImGui::SameLine(); if (ImGui::Button("Load")) ImGui::OpenPopup("not-implemented"); ImGui::Separator(); if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere(); ImGui::PushItemWidth(-0.1f); if (ImGui::InputTextWithHint("", "What do you want to do today?", s.new_todo_input.data(), s.input_string_size, ImGuiInputTextFlags_EnterReturnsTrue)) { ctx.dispatch(todo::add_todo_action{s.new_todo_input.data()}); s.new_todo_input[0] = '\0'; ImGui::SetKeyboardFocusHere(-1); } ImGui::PopItemWidth(); ImGui::Separator(); ImGui::BeginChild(""); { auto idx = std::size_t{}; for (auto item : m.todos) { ImGui::PushID(idx); auto with_idx = [idx](auto&& a) { return std::make_pair(idx, a); }; draw({ctx, with_idx}, item); ImGui::PopID(); ++idx; } } ImGui::EndChild(); ImGui::End(); } int main() { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) { std::cerr << "Error initializing SDL: " << SDL_GetError() << std::endl; return -1; } const char* glsl_version = "#version 300 es"; SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); auto current = SDL_DisplayMode{}; SDL_GetCurrentDisplayMode(0, ¤t); auto window = SDL_CreateWindow("Todo Imgui", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, window_width, window_height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_SHOWN); if (!window) { std::cerr << "Error creating SDL window: " << SDL_GetError() << std::endl; return -1; } auto gl_context = SDL_GL_CreateContext(window); if (!gl_context) { std::cerr << "Error creating GL context: " << SDL_GetError() << std::endl; return -1; } IMGUI_CHECKVERSION(); ImGui::CreateContext(); auto& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; ImGui::StyleColorsDark(); ImGui_ImplSDL2_InitForOpenGL(window, gl_context); ImGui_ImplOpenGL3_Init(glsl_version); auto loop = lager::sdl_event_loop{}; auto store = lager::make_store( todo::model{}, lager::with_sdl_event_loop{loop}); auto state = ui_state{}; loop.run( [&](const SDL_Event& ev) { ImGui_ImplSDL2_ProcessEvent(&ev); return ev.type != SDL_QUIT; }, [&](auto dt) { ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL2_NewFrame(window); ImGui::NewFrame(); { draw(store, store.get(), state); } ImGui::Render(); SDL_GL_MakeCurrent(window, gl_context); auto size = ImGui::GetIO().DisplaySize; glViewport(0, 0, (int) size.x, (int) size.y); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); SDL_GL_SwapWindow(window); }); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); SDL_GL_DeleteContext(gl_context); SDL_DestroyWindow(window); SDL_Quit(); return 0; } lager-0.1.1/example/todo/item.cpp000066400000000000000000000013651451275761400166660ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "item.hpp" #include namespace todo { item update(item s, item_action a) { return lager::match(std::move(a))( [&](toggle_item_action&& a) { s.done = !s.done; return std::move(s); }, [&](remove_item_action&& a) { return std::move(s); }); } } // namespace todo lager-0.1.1/example/todo/item.hpp000066400000000000000000000014601451275761400166670ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include #include #include namespace todo { struct item { bool done = false; std::string text; }; struct toggle_item_action {}; struct remove_item_action {}; using item_action = std::variant; item update(item m, item_action a); } // namespace todo LAGER_STRUCT(todo, item, done, text); lager-0.1.1/example/todo/model.cpp000066400000000000000000000040101451275761400170160ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "model.hpp" #include #include #include #include #include #include #include namespace todo { model update(model s, model_action a) { return lager::match(std::move(a))( [&](add_todo_action&& a) { if (!a.text.empty()) s.todos = std::move(s.todos).push_front({false, a.text}); return std::move(s); }, [&](std::pair&& a) { if (a.first >= s.todos.size()) { std::cerr << "Invalid todo::item_action index!" << std::endl; } else { s.todos = std::holds_alternative(a.second) ? std::move(s.todos).erase(a.first) : std::move(s.todos).update(a.first, [&](auto&& t) { return update(t, a.second); }); } return std::move(s); }); } void save(const std::string& fname, model todos) { auto s = std::ofstream{fname}; s.exceptions(std::fstream::badbit | std::fstream::failbit); { auto a = cereal::JSONOutputArchive{s}; save_inline(a, todos); } } model load(const std::string& fname) { auto s = std::ifstream{fname}; s.exceptions(std::fstream::badbit); auto r = model{}; { auto a = cereal::JSONInputArchive{s}; load_inline(a, r); } return r; } } // namespace todo lager-0.1.1/example/todo/model.hpp000066400000000000000000000016151451275761400170330ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include "item.hpp" #include #include namespace todo { struct model { immer::flex_vector todos; }; struct add_todo_action { std::string text; }; using model_action = std::variant>; model update(model m, model_action a); void save(const std::string& fname, model todos); model load(const std::string& fname); } // namespace todo LAGER_STRUCT(todo, model, todos); lager-0.1.1/example/todo/qml-redux/000077500000000000000000000000001451275761400171355ustar00rootroot00000000000000lager-0.1.1/example/todo/qml-redux/main.cpp000066400000000000000000000101141451275761400205620ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "../app.hpp" #include #include #include #include #include #include #include #include #include #include #include ZUG_INLINE_CONSTEXPR auto to_qstring = zug::comp([](auto&& f) { return [f](auto&& p) { return f(QString::fromStdString(ZUG_FWD(p)))( [&](auto&& x) { return ZUG_FWD(x).toStdString(); }); }; }); class Item : public QObject { Q_OBJECT lager::context ctx_; public: Item(lager::context ctx, lager::reader data) : ctx_{std::move(ctx)} , LAGER_QT(done){data[&todo::item::done]} , LAGER_QT(text){data[&todo::item::text][to_qstring]} {} LAGER_QT_READER(bool, done); LAGER_QT_READER(QString, text); Q_INVOKABLE void toggle() { ctx_.dispatch(todo::toggle_item_action{}); } Q_INVOKABLE void remove() { ctx_.dispatch(todo::remove_item_action{}); } }; class Model : public QObject { Q_OBJECT lager::context ctx_; lager::reader data_; public: Model(lager::context ctx, lager::reader data) : ctx_{std::move(ctx)} , data_{std::move(data)} , LAGER_QT(count){data_.map( [](auto&& x) { return static_cast(x.todos.size()); })} {} LAGER_QT_READER(int, count); Q_INVOKABLE Item* item(int index) { if (index < 0) return nullptr; auto idx = static_cast(index); return new Item{ {ctx_, [idx](auto a) { return std::pair{idx, a}; }}, data_[&todo::model::todos][index][lager::lenses::or_default]}; } Q_INVOKABLE void add(QString text) { ctx_.dispatch(todo::add_todo_action{text.toStdString()}); } }; class App : public lager::event_loop_quick_item { Q_OBJECT; using base_t = lager::event_loop_quick_item; todo::logger logger_{.error = [this](auto&& text) { Q_EMIT error(QString::fromStdString(text)); }}; lager::store store_ = lager::make_store( todo::app{}, lager::with_qml_event_loop{*this}, lager::with_deps(std::ref(logger_))); Model model_{store_, store_[&todo::app::doc]}; public: LAGER_QT_READER(QString, path); LAGER_QT_READER(QString, name); App(QQuickItem* parent = nullptr) : base_t{parent} , LAGER_QT(path){store_[&todo::app::path].map( [](auto&& p) { return p.string(); })[to_qstring]} , LAGER_QT(name){LAGER_QT(path).map( [](auto f) { return QFileInfo{f}.baseName(); })} {} Q_PROPERTY(Model* model READ model CONSTANT) Model* model() { return &model_; } Q_INVOKABLE void save(QUrl fname) { store_.dispatch(todo::save_action{fname.toLocalFile().toStdString()}); } Q_INVOKABLE void load(QUrl fname) { store_.dispatch(todo::load_action{fname.toLocalFile().toStdString()}); } Q_SIGNAL void error(QString text); }; #include "main.moc" int main(int argc, char** argv) { QApplication app{argc, argv}; QQmlApplicationEngine engine; qmlRegisterType("Lager.Example.Todo", 1, 0, "App"); qmlRegisterUncreatableType("Lager.Example.Todo", 1, 0, "Item", ""); qmlRegisterUncreatableType("Lager.Example.Todo", 1, 0, "Model", ""); QQuickStyle::setStyle("Material"); engine.load(LAGER_TODO_QML_DIR "/main.qml"); return app.exec(); } lager-0.1.1/example/todo/qml-redux/main.qml000066400000000000000000000076131451275761400206030ustar00rootroot00000000000000 import QtQuick 2.6 import QtQuick.Controls 2.4 import QtQuick.Controls.Material 2.4 import QtQuick.Layouts 1.11 import QtQuick.Dialogs 1.3 import Lager.Example.Todo 1.0 as Todo ApplicationWindow { width: 540 height: 960 visible: true Material.theme: Material.Dark Todo.App { id: app onError: { errorDialog.text = text; errorDialog.open() } } MessageDialog { id: errorDialog } FileDialog { id: loadFileChooser defaultSuffix: "todo" nameFilters: ["Todo files (*.todo)"] selectExisting: true onAccepted: { app.load(loadFileChooser.fileUrl) } } FileDialog { id: saveFileChooser defaultSuffix: "todo" nameFilters: ["Todo files (*.todo)"] selectExisting: false onAccepted: { app.save(saveFileChooser.fileUrl) } } Action { id: loadAction text: qsTr("&Load") shortcut: StandardKey.Load onTriggered: loadFileChooser.open() } Action { id: saveAsAction text: qsTr("Save as...") shortcut: StandardKey.SaveAs onTriggered: saveFileChooser.open() } Action { id: saveAction text: qsTr("&Save") shortcut: StandardKey.Save onTriggered: { if (app.path) app.save(app.path) else saveAsAction.trigger() } } header: ToolBar { RowLayout { anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 12 spacing: 12 ToolButton { text: qsTr("Load") action: loadAction } Label { text: app.name font.bold: true elide: Label.ElideRight horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter Layout.fillWidth: true } ToolButton { action: saveAsAction } ToolButton { action: saveAction } } } ColumnLayout { anchors.fill: parent anchors.margins: 12 TextField { Layout.fillWidth: true placeholderText: qsTr("What do you wanna do today?") onAccepted: { app.model.add(text) text = "" } Component.onCompleted: forceActiveFocus() onFocusChanged: forceActiveFocus() } ListView { Layout.fillWidth: true Layout.fillHeight: true model: app.model.count delegate: MouseArea { id: mouseArea readonly property Todo.Item todo: app.model.item(index) anchors.left: parent.left anchors.right: parent.right implicitHeight: layout.implicitHeight hoverEnabled: true onClicked: todo.toggle() RowLayout { id: layout anchors.fill: parent opacity: todo.done ? 0.5 : 1 CheckBox { checked: todo.done onClicked: { todo.done = !todo.done } } Label { Layout.fillWidth: true text: todo.text font.strikeout: todo.done font.bold: mouseArea.containsMouse } Button { text: qsTr("Delete") visible: mouseArea.containsMouse onClicked: todo.remove() } } } } } } lager-0.1.1/example/todo/qml/000077500000000000000000000000001451275761400160105ustar00rootroot00000000000000lager-0.1.1/example/todo/qml/main.cpp000066400000000000000000000066711451275761400174520ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #include "../model.hpp" #include #include #include #include #include #include #include #include #include #include class Item : public QObject { Q_OBJECT public: Item(lager::cursor data) : LAGER_QT(done){data[&todo::item::done]} , LAGER_QT(text){data[&todo::item::text].xform( zug::map([](auto&& x) { return QString::fromStdString(x); }), zug::map([](auto&& x) { return x.toStdString(); }))} {} LAGER_QT_CURSOR(bool, done); LAGER_QT_CURSOR(QString, text); }; class Model : public QObject { Q_OBJECT lager::state state_; lager::state file_name_; public: LAGER_QT_READER(QString, fileName); LAGER_QT_READER(QString, name); LAGER_QT_READER(int, count); Model() : LAGER_QT(fileName){file_name_} , LAGER_QT(name){file_name_.map( [](auto f) { return QFileInfo{f}.baseName(); })} , LAGER_QT(count){state_.xform(zug::map( [](auto&& x) { return static_cast(x.todos.size()); }))} {} Q_INVOKABLE Item* todo(int index) { return new Item{ state_[&todo::model::todos][index][lager::lenses::or_default]}; } Q_INVOKABLE void add(QString text) { if (!text.isEmpty()) { state_.update([&](auto x) { x.todos = x.todos.push_front({false, text.toStdString()}); return x; }); } } Q_INVOKABLE void remove(int index) { state_.update([&](auto x) { x.todos = x.todos.erase(index); return x; }); } Q_INVOKABLE bool save(QString fname) { try { auto fpath = QUrl{fname}.toLocalFile(); if (QFileInfo{fname}.suffix() != "todo") fpath += ".todo"; todo::save(fpath.toStdString(), *state_); file_name_.set(fname); return true; } catch (std::exception const& err) { std::cerr << "Exception thrown: " << err.what() << std::endl; return false; } } Q_INVOKABLE bool load(QString fname) { try { auto model = todo::load(QUrl{fname}.toLocalFile().toStdString()); state_.set(model); file_name_.set(fname); return true; } catch (std::exception const& err) { std::cerr << "Exception thrown: " << err.what() << std::endl; return false; } } }; #include "main.moc" int main(int argc, char** argv) { QApplication app{argc, argv}; QQmlApplicationEngine engine; qmlRegisterType("Lager.Example.Todo", 1, 0, "Model"); qmlRegisterUncreatableType("Lager.Example.Todo", 1, 0, "Item", ""); QQuickStyle::setStyle("Material"); engine.load(LAGER_TODO_QML_DIR "/main.qml"); return app.exec(); } lager-0.1.1/example/todo/qml/main.qml000066400000000000000000000102461451275761400174520ustar00rootroot00000000000000 import QtQuick 2.6 import QtQuick.Controls 2.4 import QtQuick.Controls.Material 2.4 import QtQuick.Layouts 1.11 import QtQuick.Dialogs 1.3 import Lager.Example.Todo 1.0 as Todo ApplicationWindow { width: 540 height: 960 visible: true Material.theme: Material.Dark Todo.Model { id: theModel } MessageDialog { id: saveErrorDialog text: qsTr("Could not save file") } MessageDialog { id: loadErrorDialog text: qsTr("Could not load file") } FileDialog { id: loadFileChooser defaultSuffix: "todo" nameFilters: ["Todo files (*.todo)"] selectExisting: true onAccepted: { if (!theModel.load(loadFileChooser.fileUrl)) loadErrorDialog.open() } } FileDialog { id: saveFileChooser defaultSuffix: "todo" nameFilters: ["Todo files (*.todo)"] selectExisting: false onAccepted: { if (!theModel.save(saveFileChooser.fileUrl)) saveErrorDialog.open() } } Action { id: loadAction text: qsTr("&Load") shortcut: StandardKey.Load onTriggered: loadFileChooser.open() } Action { id: saveAsAction text: qsTr("Save as...") shortcut: StandardKey.SaveAs onTriggered: saveFileChooser.open() } Action { id: saveAction text: qsTr("&Save") shortcut: StandardKey.Save onTriggered: { if (theModel.fileName) theModel.save(theModel.fileName) else saveAsAction.trigger() } } header: ToolBar { RowLayout { anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 12 spacing: 12 ToolButton { text: qsTr("Load") action: loadAction } Label { text: theModel.name font.bold: true elide: Label.ElideRight horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter Layout.fillWidth: true } ToolButton { action: saveAsAction } ToolButton { action: saveAction } } } ColumnLayout { anchors.fill: parent anchors.margins: 12 TextField { Layout.fillWidth: true placeholderText: qsTr("What do you wanna do today?") onAccepted: { theModel.add(text) text = "" } Component.onCompleted: forceActiveFocus() onFocusChanged: forceActiveFocus() } ListView { Layout.fillWidth: true Layout.fillHeight: true model: theModel.count delegate: MouseArea { id: mouseArea property Todo.Item todo: theModel.todo(index) anchors.left: parent.left anchors.right: parent.right implicitHeight: layout.implicitHeight hoverEnabled: true onClicked: { todo.done = !todo.done } RowLayout { id: layout anchors.fill: parent opacity: todo.done ? 0.5 : 1 CheckBox { checked: todo.done onClicked: { todo.done = !todo.done } } Label { Layout.fillWidth: true text: todo.text font.strikeout: todo.done font.bold: mouseArea.containsMouse } Button { text: qsTr("Delete") visible: mouseArea.containsMouse onClicked: { theModel.remove(index) } } } } } } } lager-0.1.1/lager/000077500000000000000000000000001451275761400137115ustar00rootroot00000000000000lager-0.1.1/lager/commit.hpp000066400000000000000000000024371451275761400157200ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include #include namespace lager { namespace detail { template void send_down_root(RootCursorT&& root) { (void) detail::access::roots(std::forward(root))->send_down(); } template void notify_root(RootCursorT&& root) { (void) detail::access::roots(std::forward(root))->notify(); } } // namespace detail /*! * Commit changes to a series of root cursors. All values from the root cursors * are propagated before notifying any watchers. This ensures that watchers * always see a consistent state of the world. */ template void commit(RootCursorTs&&... roots) { (detail::send_down_root(std::forward(roots)), ...); (detail::notify_root(std::forward(roots)), ...); } } // namespace lager lager-0.1.1/lager/config.hpp000066400000000000000000000023231451275761400156670ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #if (__cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) #define LAGER_HAS_CPP17 1 #endif #ifdef __has_feature #if !__has_feature(cxx_exceptions) #define LAGER_NO_EXCEPTIONS #endif #endif #ifdef LAGER_NO_EXCEPTIONS #define LAGER_TRY if (true) #define LAGER_CATCH(expr) else #define LAGER_THROW(expr) \ do { \ assert(!#expr); \ std::terminate(); \ } while (false) #define LAGER_RETHROW #else #define LAGER_TRY try #define LAGER_CATCH(expr) catch (expr) #define LAGER_THROW(expr) throw expr #define LAGER_RETHROW throw #endif lager-0.1.1/lager/constant.hpp000066400000000000000000000026361451275761400162620ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include #include namespace lager { namespace detail { template class constant_node : public root_node { using base_t = root_node; public: using base_t::base_t; virtual void recompute() final {} }; template auto make_constant_node(T&& v) { return std::make_shared>>(std::forward(v)); } } // namespace detail //! @defgroup cursors //! @{ template class constant : public reader_base> { using base_t = reader_base>; public: using value_type = T; constant(T v) : base_t{detail::make_constant_node(std::move(v))} {} private: friend class detail::access; auto roots() const { return detail::access::node(*this); } }; template auto make_constant(T&& v) -> constant> { return std::forward(v); } //! @} } // namespace lager lager-0.1.1/lager/context.hpp000066400000000000000000000241701451275761400161120ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include namespace lager { /*! * Type used to declare contexes suporting multiple action types. * * @see context */ template struct actions { constexpr static auto as_hana_ = boost::hana::tuple_t; }; /*! * Metafunction that wraps the parameter in `actions` if it is not wrapped * already, this is, it returns @a ActionOrActions if it is a type of the form * `actions` or `actions` otherwise. */ template struct as_actions { using type = actions; }; template struct as_actions> { using type = actions; }; template <> struct as_actions { using type = actions<>; }; template using as_actions_t = typename as_actions::type; namespace detail { template auto find_convertible_action_aux(Action act, Candidates candidates) { auto is_convertible = [](auto t) { return std::is_convertible{}; }; return boost::hana::find_if(candidates, is_convertible).value(); } template using find_convertible_action_t = typename decltype(find_convertible_action_aux( boost::hana::type_c, boost::hana::tuple_t))::type; template auto are_compatible_actions_aux(actions, actions, Converter c) { return boost::hana::all_of(boost::hana::tuple_t, [](auto t1) { return boost::hana::is_just( boost::hana::find_if(boost::hana::tuple_t, [](auto t2) { return std::is_convertible< decltype(c(std::declval())), typename decltype(t2)::type>{}; })); }); } template constexpr bool are_compatible_actions_v = decltype(are_compatible_actions_aux(as_actions_t{}, as_actions_t{}, std::declval()))::value; template auto merge_actions_aux(Actions1 a1, Actions2 a2) { static_assert(decltype(boost::hana::length(a1))::value > 0, ""); static_assert(decltype(boost::hana::length(a2))::value > 0, ""); auto has_convertible = [](auto seq, auto x) { return boost::hana::is_just(boost::hana::find_if(seq, [](auto t) { return std::is_convertible{}; })); }; auto remove_convertibles = [](auto seq, auto x) { return boost::hana::remove_if(seq, [](auto t) { return std::is_convertible{}; }); }; auto result = boost::hana::fold(a1, a2, [&](auto acc, auto x) { return boost::hana::if_( has_convertible(acc, x), [&] { return acc; }, [&] { auto xs = remove_convertibles(acc, x); return boost::hana::append(xs, x); })(); }); static_assert(decltype(boost::hana::length(result))::value > 0, ""); return boost::hana::if_( boost::hana::length(result) == boost::hana::size_c<1>, [&] { return result[boost::hana::size_c<0>]; }, [&] { return boost::hana::unpack(result, [](auto... xs) { return boost::hana::type_c< actions>; }); })(); } template using merge_actions_t = typename decltype(merge_actions_aux( as_actions_t::as_hana_, as_actions_t::as_hana_))::type; template struct dispatcher; template struct dispatcher> : std::function... { using std::function::operator()...; dispatcher() = default; template dispatcher(dispatcher> other) : std::function{static_cast)>&>(other)}... {} template dispatcher(Fn other) : std::function{other}... {} template static auto dispatcher_fn_aux(dispatcher> other_, Converter conv) { auto& other = static_cast, As...>)>&>(other_); return [conv, other](auto&& act) { return other(conv(LAGER_FWD(act))); }; } template dispatcher(dispatcher> other, Converter conv) : std::function{ dispatcher_fn_aux(other, conv)}... {} template dispatcher(Fn other, Converter conv) : std::function{[other, conv](auto&& act) { return other(conv(LAGER_FWD(act))); }}... {} }; struct event_loop_iface { virtual ~event_loop_iface() = default; virtual void post(std::function) = 0; virtual void async(std::function) = 0; virtual void finish() = 0; virtual void pause() = 0; virtual void resume() = 0; }; template struct event_loop_impl final : event_loop_iface { EventLoop& loop; event_loop_impl(EventLoop& loop_) : loop{loop_} {} void post(std::function fn) override { loop.post(std::move(fn)); } void async(std::function fn) override { loop.async(std::move(fn)); } void finish() override { loop.finish(); } void pause() override { loop.pause(); } void resume() override { loop.resume(); } }; } // namespace detail /*! * Provide some _context_ for effectful functions, allowing them to control the * event loop and dispatch new actions into the store. * * A context is convertible to support "more restricted" actions. This is, if * action `B` is convertible to action `A`, `context` is convertible to * `context`, in this sense, contexes are contravariant to the action type. * One can also specify multiple action types by using `action<>` tag. This is * useful to subset actions from a variant, here is an example: * * @rst * * .. code-block:: c++ * * struct action_A {}; * struct action_B {}; * struct action_C {}; * using any_action = std::variant>; * * void some_effect(context> ctx) * { * if (...) * ctx.dispatch(action_A{}); * else * ctx.dispatch(action_B{}); * } * * void other_effect(context ctx) * { * some_effect(ctx); * ... * } * * @endrst * * @note This is a reference type and it's life-time is bound to the associated * store. It is invalid to use it after the store has been destructed. * Its methods may modify the store's underlying state. * * @note Use action type `void` or empty `lager::actions<>` if `context` shall * have no `dispatch()` method and only provide deps. * * @todo Make constructors private. */ template > struct context : Deps { using deps_t = Deps; using actions_t = as_actions_t; context() = default; template < typename Actions_, typename Deps_, std::enable_if_t && std::is_convertible_v, int> = 0> context(const context& ctx) : deps_t{ctx} , dispatcher_{ctx.dispatcher_} , loop_{ctx.loop_} {} template < typename Actions_, typename Deps_, typename Converter, std::enable_if_t< detail::are_compatible_actions_v && std::is_convertible_v, int> = 0> context(const context& ctx, Converter c) : deps_t{ctx} , dispatcher_{ctx.dispatcher_, c} , loop_{ctx.loop_} {} template context(Dispatcher dispatcher, EventLoop& loop, deps_t deps) : deps_t{std::move(deps)} , dispatcher_{std::move(dispatcher)} , loop_{std::make_shared>(loop)} {} template future dispatch(Action&& act) const { return dispatcher_(std::forward(act)); } detail::event_loop_iface& loop() const { return *loop_; } private: template friend struct context; detail::dispatcher dispatcher_; std::shared_ptr loop_; }; } // namespace lager lager-0.1.1/lager/cursor.hpp000066400000000000000000000037561451275761400157520ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include #include #include #include #include #include namespace lager { //! @defgroup cursors //! @{ template struct cursor_mixin : writer_mixin , reader_mixin { using writer_mixin::operator[]; using writer_mixin::make; using writer_mixin::zoom; using writer_mixin::xform; using reader_mixin::xform; protected: ~cursor_mixin() = default; }; template class cursor_base : public cursor_mixin> , public xform_mixin> , public watchable_base { friend class detail::access; using base_t = watchable_base; public: using value_type = zug::meta::value_t; cursor_base() = default; template , zug::meta::value_t>, int> = 0> cursor_base(cursor_base x) : base_t{std::move(x)} {} template cursor_base(std::shared_ptr n) : base_t{std::move(n)} {} }; /*! * Provides access to reading and writing values of type `T`. */ template class cursor : public cursor_base> { using base_t = cursor_base>; public: using base_t::base_t; }; //! @} } // namespace lager lager-0.1.1/lager/debug/000077500000000000000000000000001451275761400147775ustar00rootroot00000000000000lager-0.1.1/lager/debug/debugger.hpp000066400000000000000000000162401451275761400172770ustar00rootroot00000000000000// // lager - library for functional interactive c++ programs // Copyright (C) 2017 Juan Pedro Bolivar Puente // // This file is part of lager. // // lager is free software: you can redistribute it and/or modify // it under the terms of the MIT License, as detailed in the LICENSE // file located at the root of this source code distribution, // or here: // #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include namespace lager { template struct debugger { using base_action = Action; using base_model = Model; using deps_t = Deps; using cursor_t = std::size_t; struct goto_action { cursor_t cursor; LAGER_STRUCT_NESTED(goto_action, cursor); }; struct undo_action { LAGER_STRUCT_NESTED(undo_action); }; struct redo_action { LAGER_STRUCT_NESTED(redo_action); }; struct pause_action { LAGER_STRUCT_NESTED(pause_action); }; struct resume_action { LAGER_STRUCT_NESTED(resume_action); }; using action = std::variant; struct step { Action action; Model model; LAGER_STRUCT_NESTED(step, action, model); }; struct model { cursor_t cursor = {}; bool paused = {}; Model init; immer::vector history = {}; immer::vector pending = {}; LAGER_STRUCT_NESTED(model, cursor, paused, init, history, pending); model() = default; model(Model i) : init{i} {} using lookup_result = std::pair, const Model&>; lookup_result lookup(cursor_t cursor) const { if (cursor > history.size()) LAGER_THROW(std::runtime_error{"bad cursor"}); return cursor == 0 ? lookup_result{{}, init} : [&] { auto& step = history[cursor - 1]; return lookup_result{step.action, step.model}; }(); } std::size_t summary() const { return history.size(); } operator const Model&() const { return lookup(cursor).second; } friend decltype(auto) unwrap(const model& m) { return unwrap(static_cast(m)); } }; using result_t = std::pair>; template static result_t update(ReducerFn&& reducer, model m, action act) { return std::visit( visitor{ [&](Action act) -> result_t { if (m.paused) { m.pending = m.pending.push_back(act); return {m, noop}; } else { auto eff = effect{noop}; auto state = invoke_reducer( reducer, m, act, [&](auto&& e) { eff = LAGER_FWD(e); }, [] {}); m.history = m.history.take(m.cursor).push_back( {act, std::move(state)}); m.cursor = m.history.size(); return {m, eff}; } }, [&](goto_action act) -> result_t { if (act.cursor <= m.history.size()) m.cursor = act.cursor; return {m, noop}; }, [&](undo_action) -> result_t { if (m.cursor > 0) --m.cursor; return {m, noop}; }, [&](redo_action) -> result_t { if ((m.cursor) < m.history.size()) ++m.cursor; return {m, noop}; }, [&](pause_action) -> result_t { m.paused = true; return {m, [](auto&& ctx) { ctx.loop().pause(); }}; }, [&](resume_action) -> result_t { auto resume_eff = effect{[](auto&& ctx) { ctx.loop().resume(); }}; auto eff = effect{noop}; auto pending = m.pending; m.paused = false; m.pending = {}; std::tie(m, eff) = immer::accumulate( pending, std::pair{m, eff}, [&](result_t acc, auto&& act) -> result_t { auto [m, eff] = LAGER_FWD(acc); auto [new_m, new_eff] = update(reducer, std::move(m), LAGER_FWD(act)); return {new_m, sequence(eff, new_eff)}; }); return {m, sequence(resume_eff, eff)}; }, }, act); } template static void view(Server& serv, const model& m) { serv.view(m); } }; template