diff --git a/lib/bindey/.clang-format b/lib/bindey/.clang-format new file mode 100644 index 00000000..c3b36785 --- /dev/null +++ b/lib/bindey/.clang-format @@ -0,0 +1,72 @@ +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: true +AlignEscapedNewlinesLeft: true +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 120 +CommentPragmas: '^!' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 0 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +ForEachMacros: [ foreach, BOOST_FOREACH ] +IndentCaseLabels: true +IndentFunctionDeclarationAfterType: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +Language: Cpp +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: None +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 100 +PenaltyReturnTypeOnItsOwnLine: 600 +PointerAlignment: Left +SpaceAfterCStyleCast: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: true +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never diff --git a/lib/bindey/.github/workflows/ci.yml b/lib/bindey/.github/workflows/ci.yml new file mode 100644 index 00000000..060f14e4 --- /dev/null +++ b/lib/bindey/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: ci + +on: [pull_request] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + Build-And-Test: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - name: Create Build Environment + # Some projects don't allow in-source building, so create a separate build directory + # We'll use this as our working directory for all subsequent commands + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure + shell: bash + working-directory: ${{runner.workspace}}/build + run: cmake $GITHUB_WORKSPACE -GXcode -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBINDEY_BUILD_TESTS=ON + env: + CC: clang + CXX: clang + + - name: Build + working-directory: ${{runner.workspace}}/build + shell: bash + run: cmake --build . --config $BUILD_TYPE + env: + CC: clang + CXX: clang + + - name: Test + working-directory: ${{runner.workspace}}/build + shell: bash + # Execute tests defined by the CMake configuration. + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest -C $BUILD_TYPE diff --git a/lib/bindey/.gitignore b/lib/bindey/.gitignore new file mode 100644 index 00000000..92b4043c --- /dev/null +++ b/lib/bindey/.gitignore @@ -0,0 +1,5 @@ +*.a +*.lib +*.o +*.pdb +.DS_Store diff --git a/lib/bindey/.gitmodules b/lib/bindey/.gitmodules new file mode 100644 index 00000000..67f59dd4 --- /dev/null +++ b/lib/bindey/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/Catch2"] + path = lib/Catch2 + url = git@github.com:catchorg/Catch2.git +[submodule "lib/nod"] + path = lib/nod + url = git@github.com:fr00b0/nod.git diff --git a/lib/bindey/CMakeLists.txt b/lib/bindey/CMakeLists.txt new file mode 100644 index 00000000..f71c176a --- /dev/null +++ b/lib/bindey/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright 2023 jacqueline +# +# SPDX-License-Identifier: GPL-3.0-only +idf_component_register( + INCLUDE_DIRS "include" +) diff --git a/lib/bindey/LICENSE.md b/lib/bindey/LICENSE.md new file mode 100644 index 00000000..5ba17155 --- /dev/null +++ b/lib/bindey/LICENSE.md @@ -0,0 +1,21 @@ +## The MIT License (MIT) + +Copyright (c) 2021 Kevin Dixon + +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. diff --git a/lib/bindey/README.md b/lib/bindey/README.md new file mode 100644 index 00000000..0ef0e62b --- /dev/null +++ b/lib/bindey/README.md @@ -0,0 +1,116 @@ +# bindey + +Everyone knows Model-View-ViewModel is the best architecture, but how can we realize it in C++ applications with minimal overhead, and no complicated framework impositions? + +`bindey` provides the basic building block of MVVM -- an observable "Property" and a databinding mechanism. + +## Property Usage + +At minimum, `bindey::property` can allow you to avoid writing getters and setters. Consider this example: + +``` +#include + +using namespace bindey; + +class Person +{ +public: + property name; + property age; +}; +``` +Then we can use it like this: +``` +Person p; +p.name("Kevin"); +p.age(666); + +auto thatDudesName = p.name(); +auto ageIsJustANumber = p.age(); +``` + +`property` default initializes its value with `{}`, and of course allows initialization. +``` +Person::Person() +: name("Default Name") +, age(0) +{} +``` +## Data Binding +`bindey` provides a simple binding mechanism to connect a "source" `property` to an arbitrary object. This base signature is +``` +template +binding bind( property& from, To& to ); +``` +And a specialization for `property` to `property` binding of the same type is provided. +``` +template +binding bind( property& from, property& to ) +{ + return from.onChanged( [&]( const auto& newValue ) { to( newValue ); } ); +} +``` + +### Writing Your Own Bindings +Where this becomes fun is when you get to reduce boilerplate. For example, assume a `Button` class from some UI Framework. +``` +struct Button +{ + void setText(const std::string& text) + { + this->text = text; + } + + std::string text; +}; +``` +To make your life better, simply implement a template speciailization in the `bindey` namespace. +``` +namespace bindey +{ +template <> +binding bind( property& from, Button& to ) +{ + return from.onChanged( [&]( const auto& newValue ){ to.setText( newValue ); } ); +} +} // namespace bindey +``` +Then, bind your property to the button as needed: +``` +bindey::property name; +... +Button someButton; +... +bindey::bind( name, someButton ); +``` + +### Binding Lifetimes +The result of a call to `bind` is a `bindey::binding` object. If this return value is discarded, then the binding's lifetime is coupled to the `property`'s. + +Otherwise, this token can be used to disconnect the binding as needed, the easiest way is to capture it in a `scoped_binding` object. + +For example, if your binding involves objects who's lifetime you do not control, you should certainly capture the binding to avoid crashes. +``` +struct GreatObj +{ + GreatObj(Button* b) + { + mSomeButton = b; + mButtonBinding = bindey::bind( name, *mSomeButton ); + } + + void updateButton(Button* newB) + { + mSomeButton = nullptr; + mButtonBinding = {}; // disconnect from old button + if( newB != nullptr ) + { + mSomeButton = newB; + mButtonBinding = bindey::bind( name, *mSomeButton ); + } + } + + bindey::scoped_binding mButtonBinding; +}; +``` diff --git a/lib/bindey/include/bindey/binding.h b/lib/bindey/include/bindey/binding.h new file mode 100644 index 00000000..69baa5cf --- /dev/null +++ b/lib/bindey/include/bindey/binding.h @@ -0,0 +1,47 @@ +#pragma once + +#include "property.h" + +#include + +#include +#include + +namespace bindey +{ + +using binding = nod::connection; +using scoped_binding = nod::scoped_connection; + +/** + * base binding signature + */ +template +binding bind( property& from, To& to ); + +/** + * binds two properties of the same type + */ +template +binding bind( property& from, property& to ) +{ + return from.onChanged( [&]( const auto& newValue ) { to( newValue ); } ); +} + +/** + * binds two properties of differing types using a Converter callable + * @param from property to observe + * @param to property to write to + * @param bindingConverter a callable to invoke to convert between the types + */ +template +binding bind( property& from, property& to, Converter&& bindingConverter ) +{ + static_assert( std::is_convertible>::value, + "Wrong Signature for binding converter!" ); + + return from.onChanged( + [&to, converter = bindingConverter]( const auto& newValue ) { to( converter( newValue ) ); } ); +} + +} // namespace bindey diff --git a/lib/bindey/include/bindey/property.h b/lib/bindey/include/bindey/property.h new file mode 100644 index 00000000..369f01f0 --- /dev/null +++ b/lib/bindey/include/bindey/property.h @@ -0,0 +1,137 @@ +#pragma once + +#include + +#include + +namespace bindey +{ + +/** + * Optional always_update policy to notify subscribers everytime the property value is set, not just when it changes + */ +class always_update +{ +public: + template + bool operator()( const T&, const T& ) const + { + return true; + } +}; + +template , + typename Signal = nod::unsafe_signal> +class property +{ +public: + property() + { + } + property( T&& value ) + : mStorage( std::move( value ) ) + { + } + property( const property& ) = delete; + property& operator=(const property&) = delete; + + + /** + * gets the current value + * @return const reference to the value + */ + const T& get() const + { + return mStorage; + } + + /** + * gets the current value + * @return mutable reference to the value + */ + T& get() + { + return mStorage; + } + + const T& operator()() const + { + return get(); + } + + T& operator()() + { + return get(); + } + + /** + * sets the value of the property. + * @param value the new value + * @discussion the value will only be updated if the UpdatePolicy's critera is met. + * if the value is changed, then the @ref changed event will be fired. + */ + void set( const T& value ) + { + if ( UpdatePolicy{}( mStorage, value ) ) + { + mStorage = value; + changed( mStorage ); + } + } + + void set( T&& value ) + { + if ( UpdatePolicy{}( mStorage, value ) ) + { + mStorage = std::move( value ); + changed( mStorage ); + } + } + + void operator()( const T& value ) + { + set( value ); + } + + void operator()( T&& value ) + { + set( std::move( value ) ); + } + + /** + * this signal is invoked whenever the the value changes per the UpdatePolicy + * @discussion nod::unsafe_signal is used here for speed. Take care of your own threading. + */ + Signal changed; + + /** + * convience function to attach a change listener to this property + */ + auto onChanged( typename decltype( changed )::slot_type&& c ) + { + return changed.connect( std::move( c ) ); + } + + /** + * convience function to attach a change listener to this property and call it right away + */ + auto onChangedAndNow( typename decltype( changed )::slot_type&& c ) + { + auto connection = onChanged( std::move( c ) ); + changed( mStorage ); + return connection; + } + + +private: + T mStorage{}; +}; + +/** + * thread safe property type based on nod::signal + */ +template > +using safe_property = property>; + +} // namespace bindey diff --git a/lib/bindey/include/nod/nod.hpp b/lib/bindey/include/nod/nod.hpp new file mode 100644 index 00000000..68e7b8d4 --- /dev/null +++ b/lib/bindey/include/nod/nod.hpp @@ -0,0 +1,681 @@ +#ifndef IG_NOD_INCLUDE_NOD_HPP +#define IG_NOD_INCLUDE_NOD_HPP + +#include // std::vector +#include // std::function +#include // std::mutex, std::lock_guard +#include // std::shared_ptr, std::weak_ptr +#include // std::find_if() +#include // assert() +#include // std::this_thread::yield() +#include // std::is_same +#include // std::back_inserter + +namespace nod { + // implementational details + namespace detail { + /// Interface for type erasure when disconnecting slots + struct disconnector { + virtual ~disconnector() {} + virtual void operator()( std::size_t index ) const = 0; + }; + /// Deleter that doesn't delete + inline void no_delete(disconnector*){ + }; + } // namespace detail + + /// Base template for the signal class + template + class signal_type; + + + /// Connection class. + /// + /// This is used to be able to disconnect slots after they have been connected. + /// Used as return type for the connect method of the signals. + /// + /// Connections are default constructible. + /// Connections are not copy constructible or copy assignable. + /// Connections are move constructible and move assignable. + /// + class connection { + public: + /// Default constructor + connection() : + _index() + {} + + // Connection are not copy constructible or copy assignable + connection( connection const& ) = delete; + connection& operator=( connection const& ) = delete; + + /// Move constructor + /// @param other The instance to move from. + connection( connection&& other ) : + _weak_disconnector( std::move(other._weak_disconnector) ), + _index( other._index ) + {} + + /// Move assign operator. + /// @param other The instance to move from. + connection& operator=( connection&& other ) { + _weak_disconnector = std::move( other._weak_disconnector ); + _index = other._index; + return *this; + } + + /// @returns `true` if the connection is connected to a signal object, + /// and `false` otherwise. + bool connected() const { + return !_weak_disconnector.expired(); + } + + /// Disconnect the slot from the connection. + /// + /// If the connection represents a slot that is connected to a signal object, calling + /// this method will disconnect the slot from that object. The result of this operation + /// is that the slot will stop receiving calls when the signal is invoked. + void disconnect(); + + private: + /// The signal template is a friend of the connection, since it is the + /// only one allowed to create instances using the meaningful constructor. + template friend class signal_type; + + /// Create a connection. + /// @param shared_disconnector Disconnector instance that will be used to disconnect + /// the connection when the time comes. A weak pointer + /// to the disconnector will be held within the connection + /// object. + /// @param index The slot index of the connection. + connection( std::shared_ptr const& shared_disconnector, std::size_t index ) : + _weak_disconnector( shared_disconnector ), + _index( index ) + {} + + /// Weak pointer to the current disconnector functor. + std::weak_ptr _weak_disconnector; + /// Slot index of the connected slot. + std::size_t _index; + }; + + /// Scoped connection class. + /// + /// This type of connection is automatically disconnected when + /// the connection object is destructed. + /// + class scoped_connection + { + public: + /// Scoped are default constructible + scoped_connection() = default; + /// Scoped connections are not copy constructible + scoped_connection( scoped_connection const& ) = delete; + /// Scoped connections are not copy assingable + scoped_connection& operator=( scoped_connection const& ) = delete; + + /// Move constructor + scoped_connection( scoped_connection&& other ) : + _connection( std::move(other._connection) ) + {} + + /// Move assign operator. + /// @param other The instance to move from. + scoped_connection& operator=( scoped_connection&& other ) { + reset( std::move( other._connection ) ); + return *this; + } + + /// Construct a scoped connection from a connection object + /// @param connection The connection object to manage + scoped_connection( connection&& c ) : + _connection( std::forward(c) ) + {} + + /// destructor + ~scoped_connection() { + disconnect(); + } + + /// Assignment operator moving a new connection into the instance. + /// @note If the scoped_connection instance already contains a + /// connection, that connection will be disconnected as if + /// the scoped_connection was destroyed. + /// @param c New connection to manage + scoped_connection& operator=( connection&& c ) { + reset( std::forward(c) ); + return *this; + } + + /// Reset the underlying connection to another connection. + /// @note The connection currently managed by the scoped_connection + /// instance will be disconnected when resetting. + /// @param c New connection to manage + void reset( connection&& c = {} ) { + disconnect(); + _connection = std::move(c); + } + + /// Release the underlying connection, without disconnecting it. + /// @returns The newly released connection instance is returned. + connection release() { + connection c = std::move(_connection); + _connection = connection{}; + return c; + } + + /// + /// @returns `true` if the connection is connected to a signal object, + /// and `false` otherwise. + bool connected() const { + return _connection.connected(); + } + + /// Disconnect the slot from the connection. + /// + /// If the connection represents a slot that is connected to a signal object, calling + /// this method will disconnect the slot from that object. The result of this operation + /// is that the slot will stop receiving calls when the signal is invoked. + void disconnect() { + _connection.disconnect(); + } + + private: + /// Underlying connection object + connection _connection; + }; + + /// Policy for multi threaded use of signals. + /// + /// This policy provides mutex and lock types for use in + /// a multithreaded environment, where signals and slots + /// may exists in different threads. + /// + /// This policy is used in the `nod::signal` type provided + /// by the library. + struct multithread_policy + { + using mutex_type = std::mutex; + using mutex_lock_type = std::unique_lock; + /// Function that yields the current thread, allowing + /// the OS to reschedule. + static void yield_thread() { + std::this_thread::yield(); + } + /// Function that defers a lock to a lock function that prevents deadlock + static mutex_lock_type defer_lock(mutex_type & m){ + return mutex_lock_type{m, std::defer_lock}; + } + /// Function that locks two mutexes and prevents deadlock + static void lock(mutex_lock_type & a,mutex_lock_type & b) { + std::lock(a,b); + } + }; + + /// Policy for single threaded use of signals. + /// + /// This policy provides dummy implementations for mutex + /// and lock types, resulting in that no synchronization + /// will take place. + /// + /// This policy is used in the `nod::unsafe_signal` type + /// provided by the library. + struct singlethread_policy + { + /// Dummy mutex type that doesn't do anything + struct mutex_type{}; + /// Dummy lock type, that doesn't do any locking. + struct mutex_lock_type + { + /// A lock type must be constructible from a + /// mutex type from the same thread policy. + explicit mutex_lock_type( mutex_type const& ) { + } + }; + /// Dummy implementation of thread yielding, that + /// doesn't do any actual yielding. + static void yield_thread() { + } + /// Dummy implemention of defer_lock that doesn't + /// do anything + static mutex_lock_type defer_lock(mutex_type &m){ + return mutex_lock_type{m}; + } + /// Dummy implemention of lock that doesn't + /// do anything + static void lock(mutex_lock_type &,mutex_lock_type &) { + } + }; + + /// Signal accumulator class template. + /// + /// This acts sort of as a proxy for triggering a signal and + /// accumulating the slot return values. + /// + /// This class is not really intended to instantiate by client code. + /// Instances are aquired as return values of the method `accumulate()` + /// called on signals. + /// + /// @tparam S Type of signal. The signal_accumulator acts + /// as a type of proxy for a signal instance of + /// this type. + /// @tparam T Type of initial value of the accumulate algorithm. + /// This type must meet the requirements of `CopyAssignable` + /// and `CopyConstructible` + /// @tparam F Type of accumulation function. + /// @tparam A... Argument types of the underlying signal type. + /// + template + class signal_accumulator + { + public: + /// Result type when calling the accumulating function operator. + using result_type = typename std::result_of::type; + + /// Construct a signal_accumulator as a proxy to a given signal + // + /// @param signal Signal instance. + /// @param init Initial value of the accumulate algorithm. + /// @param func Binary operation function object that will be + /// applied to all slot return values. + /// The signature of the function should be + /// equivalent of the following: + /// `R func( T1 const& a, T2 const& b )` + /// - The signature does not need to have `const&`. + /// - The initial value, type `T`, must be implicitly + /// convertible to `R` + /// - The return type `R` must be implicitly convertible + /// to type `T1`. + /// - The type `R` must be `CopyAssignable`. + /// - The type `S::slot_type::result_type` (return type of + /// the signals slots) must be implicitly convertible to + /// type `T2`. + signal_accumulator( S const& signal, T init, F func ) : + _signal( signal ), + _init( init ), + _func( func ) + {} + + /// Function call operator. + /// + /// Calling this will trigger the underlying signal and accumulate + /// all of the connected slots return values with the current + /// initial value and accumulator function. + /// + /// When called, this will invoke the accumulator function will + /// be called for each return value of the slots. The semantics + /// are similar to the `std::accumulate` algorithm. + /// + /// @param args Arguments to propagate to the slots of the + /// underlying when triggering the signal. + result_type operator()( A const& ... args ) const { + return _signal.trigger_with_accumulator( _init, _func, args... ); + } + + private: + + /// Reference to the underlying signal to proxy. + S const& _signal; + /// Initial value of the accumulate algorithm. + T _init; + /// Accumulator function. + F _func; + + }; + + /// Signal template specialization. + /// + /// This is the main signal implementation, and it is used to + /// implement the observer pattern whithout the overhead + /// boilerplate code that typically comes with it. + /// + /// Any function or function object is considered a slot, and + /// can be connected to a signal instance, as long as the signature + /// of the slot matches the signature of the signal. + /// + /// @tparam P Threading policy for the signal. + /// A threading policy must provide two type definitions: + /// - P::mutex_type, this type will be used as a mutex + /// in the signal_type class template. + /// - P::mutex_lock_type, this type must implement a + /// constructor that takes a P::mutex_type as a parameter, + /// and it must have the semantics of a scoped mutex lock + /// like std::lock_guard, i.e. locking in the constructor + /// and unlocking in the destructor. + /// + /// @tparam R Return value type of the slots connected to the signal. + /// @tparam A... Argument types of the slots connected to the signal. + template + class signal_type + { + public: + /// signals are not copy constructible + signal_type( signal_type const& ) = delete; + /// signals are not copy assignable + signal_type& operator=( signal_type const& ) = delete; + /// signals are move constructible + signal_type(signal_type&& other) + { + mutex_lock_type lock{other._mutex}; + _slot_count = std::move(other._slot_count); + _slots = std::move(other._slots); + if(other._shared_disconnector != nullptr) + { + _disconnector = disconnector{ this }; + _shared_disconnector = std::move(other._shared_disconnector); + // replace the disconnector with our own disconnector + *static_cast(_shared_disconnector.get()) = _disconnector; + } + } + /// signals are move assignable + signal_type& operator=(signal_type&& other) + { + auto lock = thread_policy::defer_lock(_mutex); + auto other_lock = thread_policy::defer_lock(other._mutex); + thread_policy::lock(lock,other_lock); + + _slot_count = std::move(other._slot_count); + _slots = std::move(other._slots); + if(other._shared_disconnector != nullptr) + { + _disconnector = disconnector{ this }; + _shared_disconnector = std::move(other._shared_disconnector); + // replace the disconnector with our own disconnector + *static_cast(_shared_disconnector.get()) = _disconnector; + } + return *this; + } + + /// signals are default constructible + signal_type() : + _slot_count(0) + {} + + // Destruct the signal object. + ~signal_type() { + invalidate_disconnector(); + } + + /// Type that will be used to store the slots for this signal type. + using slot_type = std::function; + /// Type that is used for counting the slots connected to this signal. + using size_type = typename std::vector::size_type; + + + /// Connect a new slot to the signal. + /// + /// The connected slot will be called every time the signal + /// is triggered. + /// @param slot The slot to connect. This must be a callable with + /// the same signature as the signal itself. + /// @return A connection object is returned, and can be used to + /// disconnect the slot. + template + connection connect( T&& slot ) { + mutex_lock_type lock{ _mutex }; + _slots.push_back( std::forward(slot) ); + std::size_t index = _slots.size()-1; + if( _shared_disconnector == nullptr ) { + _disconnector = disconnector{ this }; + _shared_disconnector = std::shared_ptr{&_disconnector, detail::no_delete}; + } + ++_slot_count; + return connection{ _shared_disconnector, index }; + } + + /// Function call operator. + /// + /// Calling this is how the signal is triggered and the + /// connected slots are called. + /// + /// @note The slots will be called in the order they were + /// connected to the signal. + /// + /// @param args Arguments that will be propagated to the + /// connected slots when they are called. + void operator()( A const&... args ) const { + for( auto const& slot : copy_slots() ) { + if( slot ) { + slot( args... ); + } + } + } + + /// Construct a accumulator proxy object for the signal. + /// + /// The intended purpose of this function is to create a function + /// object that can be used to trigger the signal and accumulate + /// all the slot return values. + /// + /// The algorithm used to accumulate slot return values is similar + /// to `std::accumulate`. A given binary function is called for + /// each return value with the parameters consisting of the + /// return value of the accumulator function applied to the + /// previous slots return value, and the current slots return value. + /// A initial value must be provided for the first slot return type. + /// + /// @note This can only be used on signals that have slots with + /// non-void return types, since we can't accumulate void + /// values. + /// + /// @tparam T The type of the initial value given to the accumulator. + /// @tparam F The accumulator function type. + /// @param init Initial value given to the accumulator. + /// @param op Binary operator function object to apply by the accumulator. + /// The signature of the function should be + /// equivalent of the following: + /// `R func( T1 const& a, T2 const& b )` + /// - The signature does not need to have `const&`. + /// - The initial value, type `T`, must be implicitly + /// convertible to `R` + /// - The return type `R` must be implicitly convertible + /// to type `T1`. + /// - The type `R` must be `CopyAssignable`. + /// - The type `S::slot_type::result_type` (return type of + /// the signals slots) must be implicitly convertible to + /// type `T2`. + template + signal_accumulator accumulate( T init, F op ) const { + static_assert( std::is_same::value == false, "Unable to accumulate slot return values with 'void' as return type." ); + return { *this, init, op }; + } + + + /// Trigger the signal, calling the slots and aggregate all + /// the slot return values into a container. + /// + /// @tparam C The type of container. This type must be + /// `DefaultConstructible`, and usable with + /// `std::back_insert_iterator`. Additionally it + /// must be either copyable or moveable. + /// @param args The arguments to propagate to the slots. + template + C aggregate( A const&... args ) const { + static_assert( std::is_same::value == false, "Unable to aggregate slot return values with 'void' as return type." ); + C container; + auto iterator = std::back_inserter( container ); + for( auto const& slot : copy_slots() ) { + if( slot ) { + (*iterator) = slot( args... ); + } + } + return container; + } + + /// Count the number of slots connected to this signal + /// @returns The number of connected slots + size_type slot_count() const { + return _slot_count; + } + + /// Determine if the signal is empty, i.e. no slots are connected + /// to it. + /// @returns `true` is returned if the signal has no connected + /// slots, and `false` otherwise. + bool empty() const { + return slot_count() == 0; + } + + /// Disconnects all slots + /// @note This operation invalidates all scoped_connection objects + void disconnect_all_slots() { + mutex_lock_type lock{ _mutex }; + _slots.clear(); + _slot_count = 0; + invalidate_disconnector(); + } + + private: + template friend class signal_accumulator; + /// Thread policy currently in use + using thread_policy = P; + /// Type of mutex, provided by threading policy + using mutex_type = typename thread_policy::mutex_type; + /// Type of mutex lock, provided by threading policy + using mutex_lock_type = typename thread_policy::mutex_lock_type; + + /// Invalidate the internal disconnector object in a way + /// that is safe according to the current thread policy. + /// + /// This will effectively make all current connection objects to + /// to this signal incapable of disconnecting, since they keep a + /// weak pointer to the shared disconnector object. + void invalidate_disconnector() { + // If we are unlucky, some of the connected slots + // might be in the process of disconnecting from other threads. + // If this happens, we are risking to destruct the disconnector + // object managed by our shared pointer before they are done + // disconnecting. This would be bad. To solve this problem, we + // discard the shared pointer (that is pointing to the disconnector + // object within our own instance), but keep a weak pointer to that + // instance. We then stall the destruction until all other weak + // pointers have released their "lock" (indicated by the fact that + // we will get a nullptr when locking our weak pointer). + std::weak_ptr weak{_shared_disconnector}; + _shared_disconnector.reset(); + while( weak.lock() != nullptr ) { + // we just yield here, allowing the OS to reschedule. We do + // this until all threads has released the disconnector object. + thread_policy::yield_thread(); + } + } + + /// Retrieve a copy of the current slots + /// + /// It's useful and necessary to copy the slots so we don't need + /// to hold the lock while calling the slots. If we hold the lock + /// we prevent the called slots from modifying the slots vector. + /// This simple "double buffering" will allow slots to disconnect + /// themself or other slots and connect new slots. + std::vector copy_slots() const + { + mutex_lock_type lock{ _mutex }; + return _slots; + } + + /// Implementation of the signal accumulator function call + template + typename signal_accumulator::result_type trigger_with_accumulator( T value, F& func, A const&... args ) const { + for( auto const& slot : copy_slots() ) { + if( slot ) { + value = func( value, slot( args... ) ); + } + } + return value; + } + + /// Implementation of the disconnection operation. + /// + /// This is private, and only called by the connection + /// objects created when connecting slots to this signal. + /// @param index The slot index of the slot that should + /// be disconnected. + void disconnect( std::size_t index ) { + mutex_lock_type lock( _mutex ); + assert( _slots.size() > index ); + if( _slots[ index ] != nullptr ) { + --_slot_count; + } + _slots[ index ] = slot_type{}; + while( _slots.size()>0 && !_slots.back() ) { + _slots.pop_back(); + } + } + + /// Implementation of the shared disconnection state + /// used by all connection created by signal instances. + /// + /// This inherits the @ref detail::disconnector interface + /// for type erasure. + struct disconnector : + detail::disconnector + { + /// Default constructor, resulting in a no-op disconnector. + disconnector() : + _ptr(nullptr) + {} + + /// Create a disconnector that works with a given signal instance. + /// @param ptr Pointer to the signal instance that the disconnector + /// should work with. + disconnector( signal_type* ptr ) : + _ptr( ptr ) + {} + + /// Disconnect a given slot on the current signal instance. + /// @note If the instance is default constructed, or created + /// with `nullptr` as signal pointer this operation will + /// effectively be a no-op. + /// @param index The index of the slot to disconnect. + void operator()( std::size_t index ) const override { + if( _ptr ) { + _ptr->disconnect( index ); + } + } + + /// Pointer to the current signal. + signal_type* _ptr; + }; + + /// Mutex to synchronize access to the slot vector + mutable mutex_type _mutex; + /// Vector of all connected slots + std::vector _slots; + /// Number of connected slots + size_type _slot_count; + /// Disconnector operation, used for executing disconnection in a + /// type erased manner. + disconnector _disconnector; + /// Shared pointer to the disconnector. All connection objects has a + /// weak pointer to this pointer for performing disconnections. + std::shared_ptr _shared_disconnector; + }; + + // Implementation of the disconnect operation of the connection class + inline void connection::disconnect() { + auto ptr = _weak_disconnector.lock(); + if( ptr ) { + (*ptr)( _index ); + } + _weak_disconnector.reset(); + } + + /// Signal type that is safe to use in multithreaded environments, + /// where the signal and slots exists in different threads. + /// The multithreaded policy provides mutexes and locks to synchronize + /// access to the signals internals. + /// + /// This is the recommended signal type, even for single threaded + /// environments. + template using signal = signal_type; + + /// Signal type that is unsafe in multithreaded environments. + /// No synchronizations are provided to the signal_type for accessing + /// the internals. + /// + /// Only use this signal type if you are sure that your environment is + /// single threaded and performance is of importance. + template using unsafe_signal = signal_type; +} // namespace nod + +#endif // IG_NOD_INCLUDE_NOD_HPP diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index 6573ee49..83406650 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -187,8 +187,8 @@ int CmdDbTracks(int argc, char** argv) { std::unique_ptr> res( db->GetTracks(20).get()); while (true) { - for (database::Track s : res->values()) { - std::cout << s.tags()[database::Tag::kTitle].value_or("[BLANK]") + for (const auto& s : res->values()) { + std::cout << s->tags()[database::Tag::kTitle].value_or("[BLANK]") << std::endl; } if (res->next_page()) { @@ -256,12 +256,12 @@ int CmdDbIndex(int argc, char** argv) { std::cout << "choice out of range" << std::endl; return -1; } - if (res->values().at(choice).track()) { + if (res->values().at(choice)->track()) { AppConsole::sServices->track_queue().IncludeLast( std::make_shared( AppConsole::sServices->database(), res, 0, res, choice)); } - auto cont = res->values().at(choice).Expand(20); + auto cont = res->values().at(choice)->Expand(20); if (!cont) { std::cout << "more choices than levels" << std::endl; return 0; @@ -270,10 +270,10 @@ int CmdDbIndex(int argc, char** argv) { choice_index++; } - for (database::IndexRecord r : res->values()) { - std::cout << r.text().value_or(""); - if (r.track()) { - std::cout << "\t(id:" << *r.track() << ")"; + for (const auto& r : res->values()) { + std::cout << r->text().value_or(""); + if (r->track()) { + std::cout << "\t(id:" << *r->track() << ")"; } std::cout << std::endl; } @@ -311,8 +311,8 @@ int CmdDbDump(int argc, char** argv) { std::unique_ptr> res(db->GetDump(5).get()); while (true) { - for (const std::pmr::string& s : res->values()) { - std::cout << s << std::endl; + for (const auto& s : res->values()) { + std::cout << *s << std::endl; } if (res->next_page()) { auto continuation = res->next_page().value(); diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 7751bf37..86394a37 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -103,6 +103,7 @@ void Decoder::Main() { for (;;) { if (source_->HasNewStream() || !stream_) { std::shared_ptr new_stream = source_->NextStream(); + ESP_LOGI(kTag, "decoder has new stream"); if (new_stream && BeginDecoding(new_stream)) { stream_ = new_stream; } else { diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index f71f0463..6039ff9d 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -34,6 +34,7 @@ #include "future_fetcher.hpp" #include "tag_parser.hpp" #include "tasks.hpp" +#include "track.hpp" #include "types.hpp" static const char* kTag = "SRC"; @@ -118,13 +119,13 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr { auto FatfsAudioInput::OpenFile(const std::pmr::string& path) -> bool { ESP_LOGI(kTag, "opening file %s", path.c_str()); - database::TrackTags tags; - if (!tag_parser_.ReadAndParseTags(path, &tags)) { + auto tags = tag_parser_.ReadAndParseTags(path); + if (!tags) { ESP_LOGE(kTag, "failed to read tags"); return false; } - auto stream_type = ContainerToStreamType(tags.encoding()); + auto stream_type = ContainerToStreamType(tags->encoding()); if (!stream_type.has_value()) { ESP_LOGE(kTag, "couldn't match container to stream"); return false; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index 6f17ad33..b1cacc00 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -19,6 +19,8 @@ namespace audio { +static constexpr char kTag[] = "tracks"; + TrackQueue::TrackQueue() {} auto TrackQueue::GetCurrent() const -> std::optional { @@ -202,6 +204,9 @@ auto TrackQueue::Previous() -> void { auto TrackQueue::Clear() -> void { const std::lock_guard lock(mutex_); + if (enqueued_.empty() && played_.empty()) { + return; + } QueueUpdate ev{.current_changed = !enqueued_.empty()}; played_.clear(); enqueued_.clear(); diff --git a/src/battery/include/battery.hpp b/src/battery/include/battery.hpp index 63a8a47b..32e02347 100644 --- a/src/battery/include/battery.hpp +++ b/src/battery/include/battery.hpp @@ -26,6 +26,10 @@ class Battery { struct BatteryState { uint_fast8_t percent; bool is_charging; + + bool operator==(const BatteryState& other) const { + return percent == other.percent && is_charging == other.is_charging; + } }; auto State() -> std::optional; diff --git a/src/database/database.cpp b/src/database/database.cpp index fd0e50c1..1ecd72e0 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -144,7 +144,7 @@ auto Database::Update() -> std::future { OwningSlice prefix = EncodeDataPrefix(); it->Seek(prefix.slice); while (it->Valid() && it->key().starts_with(prefix.slice)) { - std::optional track = ParseDataValue(it->value()); + std::shared_ptr track = ParseDataValue(it->value()); if (!track) { // The value was malformed. Drop this record. ESP_LOGW(kTag, "dropping malformed metadata"); @@ -159,9 +159,9 @@ auto Database::Update() -> std::future { continue; } - TrackTags tags{}; - if (!tag_parser_.ReadAndParseTags(track->filepath(), &tags) || - tags.encoding() == Container::kUnsupported) { + std::shared_ptr tags = + tag_parser_.ReadAndParseTags(track->filepath()); + if (!tags || tags->encoding() == Container::kUnsupported) { // We couldn't read the tags for this track. Either they were // malformed, or perhaps the file is missing. Either way, tombstone // this record. @@ -174,7 +174,7 @@ auto Database::Update() -> std::future { // At this point, we know that the track still exists in its original // location. All that's left to do is update any metadata about it. - uint64_t new_hash = tags.Hash(); + uint64_t new_hash = tags->Hash(); if (new_hash != track->tags_hash()) { // This track's tags have changed. Since the filepath is exactly the // same, we assume this is a legitimate correction. Update the @@ -185,7 +185,9 @@ auto Database::Update() -> std::future { dbPutHash(new_hash, track->id()); } - dbCreateIndexesForTrack({*track, tags}); + Track t{track, tags}; + + dbCreateIndexesForTrack(t); it->Next(); } @@ -197,15 +199,14 @@ auto Database::Update() -> std::future { .stage = event::UpdateProgress::Stage::kScanningForNewTracks, }); file_gatherer_.FindFiles("", [&](const std::pmr::string& path) { - TrackTags tags; - if (!tag_parser_.ReadAndParseTags(path, &tags) || - tags.encoding() == Container::kUnsupported) { + std::shared_ptr tags = tag_parser_.ReadAndParseTags(path); + if (!tags || tags->encoding() == Container::kUnsupported) { // No parseable tags; skip this fiile. return; } // Check for any existing record with the same hash. - uint64_t hash = tags.Hash(); + uint64_t hash = tags->Hash(); OwningSlice key = EncodeHashKey(hash); std::optional existing_hash; std::string raw_entry; @@ -219,33 +220,36 @@ auto Database::Update() -> std::future { TrackId id = dbMintNewTrackId(); ESP_LOGI(kTag, "recording new 0x%lx", id); - TrackData data(id, path, hash); - dbPutTrackData(data); + auto data = std::make_shared(id, path, hash); + dbPutTrackData(*data); dbPutHash(hash, id); - dbCreateIndexesForTrack({data, tags}); + auto t = std::make_shared(data, tags); + dbCreateIndexesForTrack(*t); return; } - std::optional existing_data = dbGetTrackData(*existing_hash); + std::shared_ptr existing_data = dbGetTrackData(*existing_hash); if (!existing_data) { // We found a hash that matches, but there's no data record? Weird. - TrackData new_data(*existing_hash, path, hash); - dbPutTrackData(new_data); - dbCreateIndexesForTrack({*existing_data, tags}); + auto new_data = std::make_shared(*existing_hash, path, hash); + dbPutTrackData(*new_data); + auto t = std::make_shared(new_data, tags); + dbCreateIndexesForTrack(*t); return; } if (existing_data->is_tombstoned()) { ESP_LOGI(kTag, "exhuming track %lu", existing_data->id()); dbPutTrackData(existing_data->Exhume(path)); - dbCreateIndexesForTrack({*existing_data, tags}); + auto t = std::make_shared(existing_data, tags); + dbCreateIndexesForTrack(*t); } else if (existing_data->filepath() != path) { ESP_LOGW(kTag, "tag hash collision for %s and %s", existing_data->filepath().c_str(), path.c_str()); ESP_LOGI(kTag, "hash components: %s, %s, %s", - tags.at(Tag::kTitle).value_or("no title").c_str(), - tags.at(Tag::kArtist).value_or("no artist").c_str(), - tags.at(Tag::kAlbum).value_or("no album").c_str()); + tags->at(Tag::kTitle).value_or("no title").c_str(), + tags->at(Tag::kArtist).value_or("no artist").c_str(), + tags->at(Tag::kAlbum).value_or("no album").c_str()); } }); events::Ui().Dispatch(event::UpdateFinished{}); @@ -264,26 +268,27 @@ auto Database::GetTrackPath(TrackId id) }); } -auto Database::GetTrack(TrackId id) -> std::future> { - return worker_task_->Dispatch>( - [=, this]() -> std::optional { - std::optional data = dbGetTrackData(id); +auto Database::GetTrack(TrackId id) -> std::future> { + return worker_task_->Dispatch>( + [=, this]() -> std::shared_ptr { + std::shared_ptr data = dbGetTrackData(id); if (!data || data->is_tombstoned()) { return {}; } - TrackTags tags; - if (!tag_parser_.ReadAndParseTags(data->filepath(), &tags)) { + std::shared_ptr tags = + tag_parser_.ReadAndParseTags(data->filepath()); + if (!tags) { return {}; } - return Track(*data, tags); + return std::make_shared(data, tags); }); } auto Database::GetBulkTracks(std::vector ids) - -> std::future>> { - return worker_task_->Dispatch>>( - [=, this]() -> std::vector> { - std::map id_to_track{}; + -> std::future>> { + return worker_task_->Dispatch>>( + [=, this]() -> std::vector> { + std::map> id_to_track{}; // Sort the list of ids so that we can retrieve them all in a single // iteration through the database, without re-seeking. @@ -299,16 +304,16 @@ auto Database::GetBulkTracks(std::vector ids) // This id wasn't found at all. Skip it. continue; } - std::optional track = + std::shared_ptr track = ParseRecord(it->key(), it->value()); if (track) { - id_to_track.insert({id, *track}); + id_to_track.insert({id, track}); } } // We've fetched all of the ids in the request, so now just put them // back into the order they were asked for in. - std::vector> results; + std::vector> results; for (const TrackId& id : ids) { if (id_to_track.contains(id)) { results.push_back(id_to_track.at(id)); @@ -426,7 +431,7 @@ auto Database::dbPutTrackData(const TrackData& s) -> void { } } -auto Database::dbGetTrackData(TrackId id) -> std::optional { +auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr { OwningSlice key = EncodeDataKey(id); std::string raw_val; if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) { @@ -454,7 +459,7 @@ auto Database::dbGetHash(const uint64_t& hash) -> std::optional { return ParseHashValue(raw_val); } -auto Database::dbCreateIndexesForTrack(Track track) -> void { +auto Database::dbCreateIndexesForTrack(const Track& track) -> void { for (const IndexInfo& index : GetIndexes()) { leveldb::WriteBatch writes; if (Index(index, track, &writes)) { @@ -481,7 +486,7 @@ auto Database::dbGetPage(const Continuation& c) -> Result* { // Grab results. std::optional first_key; - std::vector records; + std::vector> records; while (records.size() < c.page_size && it->Valid()) { if (!it->key().starts_with({c.prefix.data(), c.prefix.size()})) { break; @@ -489,9 +494,9 @@ auto Database::dbGetPage(const Continuation& c) -> Result* { if (!first_key) { first_key = it->key().ToString(); } - std::optional parsed = ParseRecord(it->key(), it->value()); + std::shared_ptr parsed = ParseRecord(it->key(), it->value()); if (parsed) { - records.push_back(*parsed); + records.push_back(parsed); } if (c.forward) { it->Next(); @@ -577,7 +582,7 @@ template auto Database::dbGetPage( template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional { + -> std::shared_ptr { std::optional data = ParseIndexKey(key); if (!data) { return {}; @@ -588,28 +593,29 @@ auto Database::ParseRecord(const leveldb::Slice& key, title = val.ToString(); } - return IndexRecord(*data, title, data->track); + return std::make_shared(*data, title, data->track); } template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional { - std::optional data = ParseDataValue(val); + -> std::shared_ptr { + std::shared_ptr data = ParseDataValue(val); if (!data || data->is_tombstoned()) { return {}; } - TrackTags tags; - if (!tag_parser_.ReadAndParseTags(data->filepath(), &tags)) { + std::shared_ptr tags = + tag_parser_.ReadAndParseTags(data->filepath()); + if (!tags) { return {}; } - return Track(*data, tags); + return std::make_shared(data, tags); } template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional { + -> std::shared_ptr { std::ostringstream stream; stream << "key: "; if (key.size() < 3 || key.data()[1] != '\0') { @@ -634,7 +640,7 @@ auto Database::ParseRecord(const leveldb::Slice& key, } } std::pmr::string res{stream.str(), &memory::kSpiRamResource}; - return res; + return std::make_shared(res); } IndexRecord::IndexRecord(const IndexKey& key, diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 98540f41..6ad8d318 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -48,12 +48,14 @@ struct Continuation { template class Result { public: - auto values() const -> const std::vector& { return values_; } + auto values() const -> const std::vector>& { + return values_; + } auto next_page() -> std::optional>& { return next_page_; } auto prev_page() -> std::optional>& { return prev_page_; } - Result(const std::vector&& values, + Result(const std::vector>&& values, std::optional> next, std::optional> prev) : values_(values), next_page_(next), prev_page_(prev) {} @@ -62,7 +64,7 @@ class Result { Result& operator=(const Result&) = delete; private: - std::vector values_; + std::vector> values_; std::optional> next_page_; std::optional> prev_page_; }; @@ -102,14 +104,14 @@ class Database { auto GetTrackPath(TrackId id) -> std::future>; - auto GetTrack(TrackId id) -> std::future>; + auto GetTrack(TrackId id) -> std::future>; /* * Fetches data for multiple tracks more efficiently than multiple calls to * GetTrack. */ auto GetBulkTracks(std::vector id) - -> std::future>>; + -> std::future>>; auto GetIndexes() -> std::vector; auto GetTracksByIndex(const IndexInfo& index, std::size_t page_size) @@ -145,30 +147,30 @@ class Database { auto dbEntomb(TrackId track, uint64_t hash) -> void; auto dbPutTrackData(const TrackData& s) -> void; - auto dbGetTrackData(TrackId id) -> std::optional; + auto dbGetTrackData(TrackId id) -> std::shared_ptr; auto dbPutHash(const uint64_t& hash, TrackId i) -> void; auto dbGetHash(const uint64_t& hash) -> std::optional; - auto dbCreateIndexesForTrack(Track track) -> void; + auto dbCreateIndexesForTrack(const Track& track) -> void; template auto dbGetPage(const Continuation& c) -> Result*; template auto ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional; + -> std::shared_ptr; }; template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional; + -> std::shared_ptr; template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional; + -> std::shared_ptr; template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional; + -> std::shared_ptr; } // namespace database diff --git a/src/database/include/records.hpp b/src/database/include/records.hpp index b144dece..e7d7738c 100644 --- a/src/database/include/records.hpp +++ b/src/database/include/records.hpp @@ -53,7 +53,7 @@ auto EncodeDataValue(const TrackData& track) -> OwningSlice; * Parses bytes previously encoded via EncodeDataValue back into a TrackData. * May return nullopt if parsing fails. */ -auto ParseDataValue(const leveldb::Slice& slice) -> std::optional; +auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr; /* Encodes a hash key for the specified hash. */ auto EncodeHashKey(const uint64_t& hash) -> OwningSlice; diff --git a/src/database/include/tag_parser.hpp b/src/database/include/tag_parser.hpp index d77967d8..04817c59 100644 --- a/src/database/include/tag_parser.hpp +++ b/src/database/include/tag_parser.hpp @@ -16,21 +16,21 @@ namespace database { class ITagParser { public: virtual ~ITagParser() {} - virtual auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) - -> bool = 0; + virtual auto ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr = 0; }; class GenericTagParser : public ITagParser { public: - auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) - -> bool override; + auto ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr override; }; class TagParserImpl : public ITagParser { public: TagParserImpl(); - auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) - -> bool override; + auto ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr override; private: std::map> extension_to_parser_; @@ -41,7 +41,7 @@ class TagParserImpl : public ITagParser { * cache should be slightly larger than any page sizes in the UI. */ std::mutex cache_mutex_; - util::LruCache<16, std::pmr::string, TrackTags> cache_; + util::LruCache<16, std::pmr::string, std::shared_ptr> cache_; // We could also consider keeping caches of artist name -> std::pmr::string // and similar. This hasn't been done yet, as this isn't a common workload in @@ -50,8 +50,8 @@ class TagParserImpl : public ITagParser { class OpusTagParser : public ITagParser { public: - auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) - -> bool override; + auto ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr override; }; } // namespace database diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp index 1c11ddea..3c7b20fa 100644 --- a/src/database/include/track.hpp +++ b/src/database/include/track.hpp @@ -61,12 +61,17 @@ enum class Tag { */ class TrackTags { public: - auto encoding() const -> Container { return encoding_; }; - auto encoding(Container e) -> void { encoding_ = e; }; - TrackTags() : encoding_(Container::kUnsupported), tags_(&memory::kSpiRamResource) {} + TrackTags(const TrackTags& other) = delete; + TrackTags& operator=(TrackTags& other) = delete; + + bool operator==(const TrackTags&) const = default; + + auto encoding() const -> Container { return encoding_; }; + auto encoding(Container e) -> void { encoding_ = e; }; + std::optional channels; std::optional sample_rate; std::optional bits_per_sample; @@ -85,10 +90,6 @@ class TrackTags { */ auto Hash() const -> uint64_t; - bool operator==(const TrackTags&) const = default; - TrackTags& operator=(const TrackTags&) = default; - TrackTags(const TrackTags&) = default; - private: Container encoding_; std::pmr::unordered_map tags_; @@ -139,6 +140,11 @@ class TrackData { play_count_(play_count), is_tombstoned_(is_tombstoned) {} + TrackData(TrackData&& other) = delete; + TrackData& operator=(TrackData& other) = delete; + + bool operator==(const TrackData&) const = default; + auto id() const -> TrackId { return id_; } auto filepath() const -> std::pmr::string { return filepath_; } auto play_count() const -> uint32_t { return play_count_; } @@ -158,8 +164,6 @@ class TrackData { * new location. */ auto Exhume(const std::pmr::string& new_path) const -> TrackData; - - bool operator==(const TrackData&) const = default; }; /* @@ -172,23 +176,22 @@ class TrackData { */ class Track { public: - Track(const TrackData& data, const TrackTags& tags) + Track(std::shared_ptr& data, std::shared_ptr tags) : data_(data), tags_(tags) {} - Track(const Track& other) = default; - auto data() const -> const TrackData& { return data_; } - auto tags() const -> const TrackTags& { return tags_; } - - auto TitleOrFilename() const -> std::pmr::string; + Track(Track& other) = delete; + Track& operator=(Track& other) = delete; bool operator==(const Track&) const = default; - Track operator=(const Track& other) const { return Track(other); } + + auto data() const -> const TrackData& { return *data_; } + auto tags() const -> const TrackTags& { return *tags_; } + + auto TitleOrFilename() const -> std::pmr::string; private: - const TrackData data_; - const TrackTags tags_; + std::shared_ptr data_; + std::shared_ptr tags_; }; -void swap(Track& first, Track& second); - } // namespace database diff --git a/src/database/records.cpp b/src/database/records.cpp index f493500c..103b3547 100644 --- a/src/database/records.cpp +++ b/src/database/records.cpp @@ -149,7 +149,7 @@ auto EncodeDataValue(const TrackData& track) -> OwningSlice { return OwningSlice(as_str); } -auto ParseDataValue(const leveldb::Slice& slice) -> std::optional { +auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr { CborParser parser; CborValue container; CborError err; @@ -211,7 +211,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional { return {}; } - return TrackData(id, path, hash, play_count, is_tombstoned); + return std::make_shared(id, path, hash, play_count, is_tombstoned); } /* 'H/ 0xBEEF' */ diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp index 8912690b..fe71089d 100644 --- a/src/database/tag_parser.cpp +++ b/src/database/tag_parser.cpp @@ -130,14 +130,13 @@ TagParserImpl::TagParserImpl() { extension_to_parser_["opus"] = std::make_unique(); } -auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path, - TrackTags* out) -> bool { +auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr { { std::lock_guard lock{cache_mutex_}; - std::optional cached = cache_.Get(path); + std::optional> cached = cache_.Get(path); if (cached) { - *out = *cached; - return true; + return *cached; } } @@ -152,41 +151,43 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path, } } - if (!parser->ReadAndParseTags(path, out)) { - return false; + std::shared_ptr tags = parser->ReadAndParseTags(path); + if (!tags) { + return {}; } // There wasn't a track number found in the track's tags. Try to synthesize // one from the filename, which will sometimes have a track number at the // start. - if (!out->at(Tag::kAlbumTrack)) { + if (!tags->at(Tag::kAlbumTrack)) { auto slash_pos = path.find_last_of("/"); if (slash_pos != std::pmr::string::npos && path.size() - slash_pos > 1) { - out->set(Tag::kAlbumTrack, path.substr(slash_pos + 1)); + tags->set(Tag::kAlbumTrack, path.substr(slash_pos + 1)); } } // Normalise track numbers; they're usually treated as strings, but we would // like to sort them lexicographically. - out->set(Tag::kAlbumTrack, - convert_track_number(out->at(Tag::kAlbumTrack).value_or("0"))); + tags->set(Tag::kAlbumTrack, + convert_track_number(tags->at(Tag::kAlbumTrack).value_or("0"))); { std::lock_guard lock{cache_mutex_}; - cache_.Put(path, *out); + cache_.Put(path, tags); } - return true; + return tags; } -auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path, - TrackTags* out) -> bool { +auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr { libtags::Aux aux; - aux.tags = out; + auto out = std::make_shared(); + aux.tags = out.get(); if (f_stat(path.c_str(), &aux.info) != FR_OK || f_open(&aux.file, path.c_str(), FA_READ) != FR_OK) { ESP_LOGW(kTag, "failed to open file %s", path.c_str()); - return false; + return {}; } // Fine to have this on the stack; this is only called on tasks with large // stacks anyway, due to all the string handling. @@ -205,7 +206,7 @@ auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path, if (res != 0) { // Parsing failed. ESP_LOGE(kTag, "tag parsing for %s failed, reason %d", path.c_str(), res); - return false; + return {}; } switch (ctx.format) { @@ -240,25 +241,26 @@ auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path, if (ctx.duration > 0) { out->duration = ctx.duration; } - return true; + return out; } -auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path, - TrackTags* out) -> bool { +auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr { std::pmr::string vfs_path = "/sdcard" + path; int err; OggOpusFile* f = op_test_file(vfs_path.c_str(), &err); if (f == NULL) { ESP_LOGE(kTag, "opusfile tag parsing failed: %d", err); - return false; + return {}; } const OpusTags* tags = op_tags(f, -1); if (tags == NULL) { ESP_LOGE(kTag, "no tags in opusfile"); op_free(f); - return false; + return {}; } + auto out = std::make_shared(); out->encoding(Container::kOpus); for (const auto& pair : kVorbisIdToTag) { const char* tag = opus_tags_query(tags, pair.first, 0); @@ -268,7 +270,7 @@ auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path, } op_free(f); - return true; + return out; } } // namespace database diff --git a/src/database/track.cpp b/src/database/track.cpp index a3c7dc99..f48bb8ed 100644 --- a/src/database/track.cpp +++ b/src/database/track.cpp @@ -64,12 +64,6 @@ auto TrackData::Exhume(const std::pmr::string& new_path) const -> TrackData { return TrackData(id_, new_path, tags_hash_, play_count_, false); } -void swap(Track& first, Track& second) { - Track temp = first; - first = second; - second = temp; -} - auto Track::TitleOrFilename() const -> std::pmr::string { auto title = tags().at(Tag::kTitle); if (title) { diff --git a/src/playlist/source.cpp b/src/playlist/source.cpp index 0df514e4..cf60b1c1 100644 --- a/src/playlist/source.cpp +++ b/src/playlist/source.cpp @@ -51,7 +51,7 @@ auto IndexRecordSource::Current() -> std::optional { return {}; } - return current_page_->values().at(current_item_).track(); + return current_page_->values().at(current_item_)->track(); } auto IndexRecordSource::Advance() -> std::optional { @@ -128,7 +128,7 @@ auto IndexRecordSource::Peek(std::size_t n, std::vector* out) working_item = 0; } - out->push_back(working_page->values().at(working_item).track().value()); + out->push_back(working_page->values().at(working_item)->track().value()); n--; items_added++; working_item++; diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index c4be715b..7914a5c3 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -66,6 +66,7 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "installing remaining drivers"); sServices->samd(std::unique_ptr(drivers::Samd::Create())); + vTaskDelay(pdMS_TO_TICKS(1000)); sServices->nvs( std::unique_ptr(drivers::NvsStorage::OpenSync())); sServices->touchwheel( diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 906e9e1f..e331d96f 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -7,10 +7,11 @@ idf_component_register( "wheel_encoder.cpp" "screen_track_browser.cpp" "screen_playing.cpp" "themes.cpp" "widget_top_bar.cpp" "screen.cpp" "screen_onboarding.cpp" "modal_progress.cpp" "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp" + "event_binding.cpp" "splash.c" "font_fusion.c" "font_symbols.c" "icons/battery_empty.c" "icons/battery_full.c" "icons/battery_20.c" "icons/battery_40.c" "icons/battery_60.c" "icons/battery_80.c" "icons/play.c" "icons/pause.c" "icons/bluetooth.c" INCLUDE_DIRS "include" - REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery") + REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "bindey") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/ui/event_binding.cpp b/src/ui/event_binding.cpp new file mode 100644 index 00000000..ed15ccfb --- /dev/null +++ b/src/ui/event_binding.cpp @@ -0,0 +1,23 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "event_binding.hpp" + +#include "core/lv_event.h" + +namespace ui { + +static auto event_cb(lv_event_t* ev) -> void { + EventBinding* binding = + static_cast(lv_event_get_user_data(ev)); + binding->signal()(lv_event_get_target(ev)); +} + +EventBinding::EventBinding(lv_obj_t* obj, lv_event_code_t ev) { + lv_obj_add_event_cb(obj, event_cb, ev, this); +} + +} // namespace ui diff --git a/src/ui/include/event_binding.hpp b/src/ui/include/event_binding.hpp new file mode 100644 index 00000000..19514db4 --- /dev/null +++ b/src/ui/include/event_binding.hpp @@ -0,0 +1,30 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#include "lvgl.h" + +#include "core/lv_event.h" +#include "core/lv_obj.h" +#include "nod/nod.hpp" + +namespace ui { + +class EventBinding { + public: + EventBinding(lv_obj_t* obj, lv_event_code_t ev); + + auto signal() -> nod::signal& { return signal_; } + + private: + lv_obj_t* obj_; + nod::signal signal_; +}; + +} // namespace ui diff --git a/src/ui/include/model_playback.hpp b/src/ui/include/model_playback.hpp new file mode 100644 index 00000000..f932dcfd --- /dev/null +++ b/src/ui/include/model_playback.hpp @@ -0,0 +1,26 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "bindey/property.h" + +#include "track.hpp" + +namespace ui { +namespace models { + +struct Playback { + bindey::property is_playing; + bindey::property> current_track; + bindey::property> upcoming_tracks; + + bindey::property current_track_position; + bindey::property current_track_duration; +}; + +} // namespace models +} // namespace ui \ No newline at end of file diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index 76251a72..ac7b19f8 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -8,11 +8,15 @@ #include #include +#include +#include "bindey/binding.h" #include "core/lv_group.h" #include "core/lv_obj.h" #include "core/lv_obj_tree.h" +#include "event_binding.hpp" #include "lvgl.h" +#include "nod/nod.hpp" #include "widget_top_bar.hpp" namespace ui { @@ -51,6 +55,16 @@ class Screen { auto CreateTopBar(lv_obj_t* parent, const widgets::TopBar::Configuration&) -> widgets::TopBar*; + std::pmr::vector data_bindings_; + std::pmr::vector> event_bindings_; + + template + auto lv_bind(lv_obj_t* obj, lv_event_code_t ev, T fn) -> void { + auto binding = std::make_unique(obj, ev); + binding->signal().connect(fn); + event_bindings_.push_back(std::move(binding)); + } + lv_obj_t* const root_; lv_obj_t* content_; lv_obj_t* modal_content_; diff --git a/src/ui/include/screen_playing.hpp b/src/ui/include/screen_playing.hpp index 2e29130c..fff9cc35 100644 --- a/src/ui/include/screen_playing.hpp +++ b/src/ui/include/screen_playing.hpp @@ -11,10 +11,13 @@ #include #include +#include "bindey/property.h" +#include "esp_log.h" #include "lvgl.h" #include "database.hpp" #include "future_fetcher.hpp" +#include "model_playback.hpp" #include "screen.hpp" #include "track.hpp" #include "track_queue.hpp" @@ -28,48 +31,36 @@ namespace screens { */ class Playing : public Screen { public: - explicit Playing(std::weak_ptr db, + explicit Playing(models::Playback& playback_model, + std::weak_ptr db, audio::TrackQueue& queue); ~Playing(); auto Tick() -> void override; - // Callbacks invoked by the UI state machine in response to audio events. - - auto OnTrackUpdate() -> void; - auto OnPlaybackUpdate(uint32_t, uint32_t) -> void; - auto OnQueueUpdate() -> void; - auto OnFocusAboveFold() -> void; auto OnFocusBelowFold() -> void; + Playing(const Playing&) = delete; + Playing& operator=(const Playing&) = delete; + private: auto control_button(lv_obj_t* parent, char* icon) -> lv_obj_t*; auto next_up_label(lv_obj_t* parent, const std::pmr::string& text) -> lv_obj_t*; - auto BindTrack(const database::Track& track) -> void; - auto ApplyNextUp(const std::vector& tracks) -> void; - std::weak_ptr db_; audio::TrackQueue& queue_; - std::optional track_; - std::vector next_tracks_; + bindey::property> current_track_; + bindey::property>> next_tracks_; - std::unique_ptr>> + std::unique_ptr>> new_track_; std::unique_ptr< - database::FutureFetcher>>> + database::FutureFetcher>>> new_next_tracks_; - lv_obj_t* artist_label_; - lv_obj_t* album_label_; - lv_obj_t* title_label_; - - lv_obj_t* scrubber_; - lv_obj_t* play_pause_control_; - lv_obj_t* next_up_header_; lv_obj_t* next_up_label_; lv_obj_t* next_up_hint_; diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 9980dac6..cb3e651c 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -7,13 +7,16 @@ #pragma once #include +#include #include #include #include "audio_events.hpp" #include "battery.hpp" +#include "bindey/property.h" #include "gpios.hpp" #include "lvgl_task.hpp" +#include "model_playback.hpp" #include "nvs.hpp" #include "relative_wheel.hpp" #include "screen_playing.hpp" @@ -27,6 +30,7 @@ #include "storage.hpp" #include "system_events.hpp" #include "touchwheel.hpp" +#include "track.hpp" #include "track_queue.hpp" #include "ui_events.hpp" #include "wheel_encoder.hpp" @@ -49,11 +53,11 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} - virtual void react(const system_fsm::BatteryStateChanged&); - virtual void react(const audio::PlaybackStarted&); - virtual void react(const audio::PlaybackFinished&); - virtual void react(const audio::PlaybackUpdate&) {} - virtual void react(const audio::QueueUpdate&) {} + void react(const system_fsm::BatteryStateChanged&); + void react(const audio::PlaybackStarted&); + void react(const audio::PlaybackFinished&); + void react(const audio::PlaybackUpdate&); + void react(const audio::QueueUpdate&); virtual void react(const system_fsm::KeyLockChanged&); @@ -88,6 +92,10 @@ class UiState : public tinyfsm::Fsm { static std::stack> sScreens; static std::shared_ptr sCurrentScreen; static std::shared_ptr sCurrentModal; + + static models::Playback sPlaybackModel; + + static bindey::property sPropBatteryState; }; namespace states { @@ -96,7 +104,6 @@ class Splash : public UiState { public: void exit() override; void react(const system_fsm::BootComplete&) override; - void react(const system_fsm::BatteryStateChanged&) override{}; using UiState::react; }; @@ -140,10 +147,6 @@ class Playing : public UiState { void react(const internal::BackPressed&) override; - void react(const audio::PlaybackStarted&) override; - void react(const audio::PlaybackUpdate&) override; - void react(const audio::PlaybackFinished&) override; - void react(const audio::QueueUpdate&) override; using UiState::react; }; diff --git a/src/ui/screen_playing.cpp b/src/ui/screen_playing.cpp index bd55924d..547bcf98 100644 --- a/src/ui/screen_playing.cpp +++ b/src/ui/screen_playing.cpp @@ -9,6 +9,7 @@ #include #include "audio_events.hpp" +#include "bindey/binding.h" #include "core/lv_event.h" #include "core/lv_obj.h" #include "core/lv_obj_scroll.h" @@ -35,6 +36,7 @@ #include "misc/lv_area.h" #include "misc/lv_color.h" #include "misc/lv_txt.h" +#include "model_playback.hpp" #include "track.hpp" #include "ui_events.hpp" #include "ui_fsm.hpp" @@ -46,8 +48,6 @@ namespace ui { namespace screens { -static constexpr std::size_t kMaxUpcoming = 10; - static void above_fold_focus_cb(lv_event_t* ev) { if (ev->user_data == NULL) { return; @@ -64,10 +64,6 @@ static void below_fold_focus_cb(lv_event_t* ev) { instance->OnFocusBelowFold(); } -static void play_pause_cb(lv_event_t* ev) { - events::Audio().Dispatch(audio::TogglePlayPause{}); -} - static lv_style_t scrubber_style; auto info_label(lv_obj_t* parent) -> lv_obj_t* { @@ -105,13 +101,42 @@ auto Playing::next_up_label(lv_obj_t* parent, const std::pmr::string& text) return button; } -Playing::Playing(std::weak_ptr db, audio::TrackQueue& queue) +Playing::Playing(models::Playback& playback_model, + std::weak_ptr db, + audio::TrackQueue& queue) : db_(db), queue_(queue), - track_(), + current_track_(), next_tracks_(), new_track_(), new_next_tracks_() { + data_bindings_.emplace_back(playback_model.current_track.onChangedAndNow( + [=, this](const std::optional& id) { + if (!id) { + return; + } + if (current_track_.get() && current_track_.get()->data().id() == *id) { + return; + } + auto db = db_.lock(); + if (!db) { + return; + } + new_track_.reset( + new database::FutureFetcher>( + db->GetTrack(*id))); + })); + data_bindings_.emplace_back(playback_model.upcoming_tracks.onChangedAndNow( + [=, this](const std::vector& ids) { + auto db = db_.lock(); + if (!db) { + return; + } + new_next_tracks_.reset(new database::FutureFetcher< + std::vector>>( + db->GetBulkTracks(ids))); + })); + lv_obj_set_layout(content_, LV_LAYOUT_FLEX); lv_group_set_wrap(group_, false); @@ -143,20 +168,40 @@ Playing::Playing(std::weak_ptr db, audio::TrackQueue& queue) lv_obj_set_flex_align(info_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - artist_label_ = info_label(info_container); - album_label_ = info_label(info_container); - title_label_ = info_label(info_container); + lv_obj_t* artist_label = info_label(info_container); + lv_obj_t* album_label = info_label(info_container); + lv_obj_t* title_label = info_label(info_container); - scrubber_ = lv_slider_create(above_fold_container); - lv_obj_set_size(scrubber_, lv_pct(100), 5); - lv_slider_set_range(scrubber_, 0, 100); - lv_slider_set_value(scrubber_, 0, LV_ANIM_OFF); + data_bindings_.emplace_back(current_track_.onChangedAndNow( + [=](const std::shared_ptr& t) { + if (!t) { + return; + } + lv_label_set_text( + artist_label, + t->tags().at(database::Tag::kArtist).value_or("").c_str()); + lv_label_set_text( + album_label, + t->tags().at(database::Tag::kAlbum).value_or("").c_str()); + lv_label_set_text(title_label, t->TitleOrFilename().c_str()); + })); + + lv_obj_t* scrubber = lv_slider_create(above_fold_container); + lv_obj_set_size(scrubber, lv_pct(100), 5); lv_style_init(&scrubber_style); lv_style_set_bg_color(&scrubber_style, lv_color_black()); - lv_obj_add_style(scrubber_, &scrubber_style, LV_PART_INDICATOR); + lv_obj_add_style(scrubber, &scrubber_style, LV_PART_INDICATOR); - lv_group_add_obj(group_, scrubber_); + lv_group_add_obj(group_, scrubber); + + data_bindings_.emplace_back( + playback_model.current_track_duration.onChangedAndNow([=](uint32_t d) { + lv_slider_set_range(scrubber, 0, std::max(1, d)); + })); + data_bindings_.emplace_back( + playback_model.current_track_position.onChangedAndNow( + [=](uint32_t p) { lv_slider_set_value(scrubber, p, LV_ANIM_OFF); })); lv_obj_t* controls_container = lv_obj_create(above_fold_container); lv_obj_set_size(controls_container, lv_pct(100), 20); @@ -164,15 +209,25 @@ Playing::Playing(std::weak_ptr db, audio::TrackQueue& queue) lv_obj_set_flex_align(controls_container, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - play_pause_control_ = control_button(controls_container, LV_SYMBOL_PLAY); - lv_obj_add_event_cb(play_pause_control_, play_pause_cb, LV_EVENT_CLICKED, - NULL); - lv_group_add_obj(group_, play_pause_control_); + lv_obj_t* play_pause_control = + control_button(controls_container, LV_SYMBOL_PLAY); + lv_group_add_obj(group_, play_pause_control); + lv_bind(play_pause_control, LV_EVENT_CLICKED, [=](lv_obj_t*) { + events::Audio().Dispatch(audio::TogglePlayPause{}); + }); + + lv_obj_t* track_prev = control_button(controls_container, LV_SYMBOL_PREV); + lv_group_add_obj(group_, track_prev); + lv_bind(track_prev, LV_EVENT_CLICKED, [=](lv_obj_t*) { queue_.Previous(); }); + + lv_obj_t* track_next = control_button(controls_container, LV_SYMBOL_NEXT); + lv_group_add_obj(group_, track_next); + lv_bind(track_next, LV_EVENT_CLICKED, [=](lv_obj_t*) { queue_.Next(); }); + + lv_obj_t* shuffle = control_button(controls_container, LV_SYMBOL_SHUFFLE); + lv_group_add_obj(group_, shuffle); + // lv_bind(shuffle, LV_EVENT_CLICKED, [=](lv_obj_t*) { queue_ }); - lv_group_add_obj(group_, control_button(controls_container, LV_SYMBOL_PREV)); - lv_group_add_obj(group_, control_button(controls_container, LV_SYMBOL_NEXT)); - lv_group_add_obj(group_, - control_button(controls_container, LV_SYMBOL_SHUFFLE)); lv_group_add_obj(group_, control_button(controls_container, LV_SYMBOL_LOOP)); next_up_header_ = lv_obj_create(above_fold_container); @@ -198,111 +253,56 @@ Playing::Playing(std::weak_ptr db, audio::TrackQueue& queue) lv_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - OnTrackUpdate(); - OnQueueUpdate(); -} - -Playing::~Playing() {} + data_bindings_.emplace_back(next_tracks_.onChangedAndNow( + [=](const std::vector>& tracks) { + // TODO(jacqueline): Do a proper diff to maintain selection. + int children = lv_obj_get_child_cnt(next_up_container_); + while (children > 0) { + lv_obj_del(lv_obj_get_child(next_up_container_, 0)); + children--; + } -auto Playing::OnTrackUpdate() -> void { - auto current = queue_.GetCurrent(); - if (!current) { - return; - } - if (track_ && track_->data().id() == *current) { - return; - } - auto db = db_.lock(); - if (!db) { - return; - } - new_track_.reset(new database::FutureFetcher>( - db->GetTrack(*current))); -} + if (tracks.empty()) { + lv_label_set_text(next_up_label_, "Nothing queued"); + lv_label_set_text(next_up_hint_, ""); + return; + } else { + lv_label_set_text(next_up_label_, "Next up"); + lv_label_set_text(next_up_hint_, ""); + } -auto Playing::OnPlaybackUpdate(uint32_t pos_seconds, uint32_t new_duration) - -> void { - if (!track_) { - return; - } - lv_slider_set_range(scrubber_, 0, new_duration); - lv_slider_set_value(scrubber_, pos_seconds, LV_ANIM_ON); + for (const auto& track : tracks) { + lv_group_add_obj(group_, next_up_label(next_up_container_, + track->TitleOrFilename())); + } + })); } -auto Playing::OnQueueUpdate() -> void { - OnTrackUpdate(); - auto current = queue_.GetUpcoming(kMaxUpcoming); - auto db = db_.lock(); - if (!db) { - return; - } - new_next_tracks_.reset( - new database::FutureFetcher>>( - db->GetBulkTracks(current))); -} +Playing::~Playing() {} auto Playing::Tick() -> void { if (new_track_ && new_track_->Finished()) { auto res = new_track_->Result(); new_track_.reset(); - if (res && *res) { - BindTrack(**res); + if (res) { + current_track_(*res); } } if (new_next_tracks_ && new_next_tracks_->Finished()) { auto res = new_next_tracks_->Result(); new_next_tracks_.reset(); if (res) { - std::vector filtered; + std::vector> filtered; for (const auto& t : *res) { if (t) { - filtered.push_back(*t); + filtered.push_back(t); } } - ApplyNextUp(filtered); + next_tracks_.set(filtered); } } } -auto Playing::BindTrack(const database::Track& t) -> void { - track_ = t; - - lv_label_set_text(artist_label_, - t.tags().at(database::Tag::kArtist).value_or("").c_str()); - lv_label_set_text(album_label_, - t.tags().at(database::Tag::kAlbum).value_or("").c_str()); - lv_label_set_text(title_label_, t.TitleOrFilename().c_str()); - - std::optional duration = t.tags().duration; - lv_slider_set_range(scrubber_, 0, duration.value_or(1)); - lv_slider_set_value(scrubber_, 0, LV_ANIM_OFF); -} - -auto Playing::ApplyNextUp(const std::vector& tracks) -> void { - // TODO(jacqueline): Do a proper diff to maintain selection. - int children = lv_obj_get_child_cnt(next_up_container_); - while (children > 0) { - lv_obj_del(lv_obj_get_child(next_up_container_, 0)); - children--; - } - - next_tracks_ = tracks; - - if (next_tracks_.empty()) { - lv_label_set_text(next_up_label_, "Nothing queued"); - lv_label_set_text(next_up_hint_, ""); - return; - } else { - lv_label_set_text(next_up_label_, "Next up"); - lv_label_set_text(next_up_hint_, ""); - } - - for (const auto& track : next_tracks_) { - lv_group_add_obj( - group_, next_up_label(next_up_container_, track.TitleOrFilename())); - } -} - auto Playing::OnFocusAboveFold() -> void { lv_obj_scroll_to_y(content_, 0, LV_ANIM_ON); } diff --git a/src/ui/screen_track_browser.cpp b/src/ui/screen_track_browser.cpp index 6cd92a04..8d1fe653 100644 --- a/src/ui/screen_track_browser.cpp +++ b/src/ui/screen_track_browser.cpp @@ -170,8 +170,8 @@ auto TrackBrowser::AddResults( initial_page_ = results; } - auto fn = [&](const database::IndexRecord& record) { - auto text = record.text(); + auto fn = [&](const std::shared_ptr& record) { + auto text = record->text(); if (!text) { // TODO(jacqueline): Display category-specific text. text = "[ no data ]"; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index fa4939f3..d7bb9bb7 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -19,6 +19,7 @@ #include "gpios.hpp" #include "lvgl_task.hpp" #include "modal_confirm.hpp" +#include "model_playback.hpp" #include "nvs.hpp" #include "relative_wheel.hpp" #include "screen.hpp" @@ -52,6 +53,10 @@ std::stack> UiState::sScreens; std::shared_ptr UiState::sCurrentScreen; std::shared_ptr UiState::sCurrentModal; +models::Playback UiState::sPlaybackModel; + +bindey::property UiState::sPropBatteryState; + auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { // Init LVGL first, since the display driver registers itself with LVGL. lv_init(); @@ -89,15 +94,33 @@ void UiState::react(const system_fsm::KeyLockChanged& ev) { } void UiState::react(const system_fsm::BatteryStateChanged&) { - UpdateTopBar(); + if (!sServices) { + return; + } + auto state = sServices->battery().State(); + if (state) { + sPropBatteryState.set(*state); + } } void UiState::react(const audio::PlaybackStarted&) { - UpdateTopBar(); + sPlaybackModel.is_playing.set(true); } void UiState::react(const audio::PlaybackFinished&) { - UpdateTopBar(); + sPlaybackModel.is_playing.set(false); +} + +void UiState::react(const audio::PlaybackUpdate& ev) { + sPlaybackModel.current_track_duration.set(ev.seconds_total); + sPlaybackModel.current_track_position.set(ev.seconds_elapsed); +} + +void UiState::react(const audio::QueueUpdate&) { + ESP_LOGI(kTag, "current changed!"); + auto& queue = sServices->track_queue(); + sPlaybackModel.current_track.set(queue.GetCurrent()); + sPlaybackModel.upcoming_tracks.set(queue.GetUpcoming(10)); } void UiState::UpdateTopBar() { @@ -283,21 +306,22 @@ void Browse::react(const internal::RecordSelected& ev) { } auto record = ev.page->values().at(ev.record); - if (record.track()) { - ESP_LOGI(kTag, "selected track '%s'", record.text()->c_str()); + if (record->track()) { + ESP_LOGI(kTag, "selected track '%s'", record->text()->c_str()); auto& queue = sServices->track_queue(); queue.Clear(); queue.IncludeLast(std::make_shared( sServices->database(), ev.initial_page, 0, ev.page, ev.record)); + ESP_LOGI(kTag, "transit to playing"); transit(); } else { - ESP_LOGI(kTag, "selected record '%s'", record.text()->c_str()); - auto cont = record.Expand(kRecordsPerPage); + ESP_LOGI(kTag, "selected record '%s'", record->text()->c_str()); + auto cont = record->Expand(kRecordsPerPage); if (!cont) { return; } auto query = db->GetPage(&cont.value()); - std::pmr::string title = record.text().value_or("TODO"); + std::pmr::string title = record->text().value_or("TODO"); PushScreen(std::make_shared( sServices->database(), title, std::move(query))); } @@ -329,8 +353,9 @@ void Browse::react(const system_fsm::BluetoothDevicesChanged&) { static std::shared_ptr sPlayingScreen; void Playing::entry() { - sPlayingScreen.reset( - new screens::Playing(sServices->database(), sServices->track_queue())); + ESP_LOGI(kTag, "push playing screen"); + sPlayingScreen.reset(new screens::Playing( + sPlaybackModel, sServices->database(), sServices->track_queue())); PushScreen(sPlayingScreen); } @@ -339,24 +364,6 @@ void Playing::exit() { PopScreen(); } -void Playing::react(const audio::PlaybackStarted& ev) { - UpdateTopBar(); - sPlayingScreen->OnTrackUpdate(); -} - -void Playing::react(const audio::PlaybackFinished& ev) { - UpdateTopBar(); - sPlayingScreen->OnTrackUpdate(); -} - -void Playing::react(const audio::PlaybackUpdate& ev) { - sPlayingScreen->OnPlaybackUpdate(ev.seconds_elapsed, ev.seconds_total); -} - -void Playing::react(const audio::QueueUpdate& ev) { - sPlayingScreen->OnQueueUpdate(); -} - void Playing::react(const internal::BackPressed& ev) { transit(); } diff --git a/tools/cmake/common.cmake b/tools/cmake/common.cmake index 313961a3..34bd1226 100644 --- a/tools/cmake/common.cmake +++ b/tools/cmake/common.cmake @@ -25,6 +25,7 @@ list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/result") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/shared_string") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/span") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tinyfsm") +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/bindey") include($ENV{IDF_PATH}/tools/cmake/project.cmake)