parent
0e04eb918e
commit
7cdcd44e0c
@ -1,72 +0,0 @@ |
||||
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 |
@ -1,44 +0,0 @@ |
||||
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 |
@ -1,5 +0,0 @@ |
||||
*.a |
||||
*.lib |
||||
*.o |
||||
*.pdb |
||||
.DS_Store |
@ -1,6 +0,0 @@ |
||||
[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 |
@ -1,6 +0,0 @@ |
||||
# Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
# |
||||
# SPDX-License-Identifier: GPL-3.0-only |
||||
idf_component_register( |
||||
INCLUDE_DIRS "include" |
||||
) |
@ -1,21 +0,0 @@ |
||||
## 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. |
@ -1,116 +0,0 @@ |
||||
# 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 <bindey/property.h> |
||||
|
||||
using namespace bindey; |
||||
|
||||
class Person |
||||
{ |
||||
public: |
||||
property<std::string> name; |
||||
property<int> 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 <typename T, typename To> |
||||
binding bind( property<T>& from, To& to ); |
||||
``` |
||||
And a specialization for `property` to `property` binding of the same type is provided. |
||||
``` |
||||
template<typename T> |
||||
binding bind( property<T>& from, property<T>& 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<std::string>& 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<std::string> 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; |
||||
}; |
||||
``` |
@ -1,47 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include "property.h" |
||||
|
||||
#include <nod/nod.hpp> |
||||
|
||||
#include <functional> |
||||
#include <type_traits> |
||||
|
||||
namespace bindey |
||||
{ |
||||
|
||||
using binding = nod::connection; |
||||
using scoped_binding = nod::scoped_connection; |
||||
|
||||
/**
|
||||
* base binding signature |
||||
*/ |
||||
template <typename T, typename To> |
||||
binding bind( property<T>& from, To& to ); |
||||
|
||||
/**
|
||||
* binds two properties of the same type |
||||
*/ |
||||
template <typename T> |
||||
binding bind( property<T>& from, property<T>& 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 <typename TFrom, typename TTo, typename Converter> |
||||
binding bind( property<TFrom>& from, property<TTo>& to, Converter&& bindingConverter ) |
||||
{ |
||||
static_assert( std::is_convertible<Converter&&, std::function<TTo( const TFrom& )>>::value, |
||||
"Wrong Signature for binding converter!" ); |
||||
|
||||
return from.onChanged( |
||||
[&to, converter = bindingConverter]( const auto& newValue ) { to( converter( newValue ) ); } ); |
||||
} |
||||
|
||||
} // namespace bindey
|
@ -1,137 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <nod/nod.hpp> |
||||
|
||||
#include <functional> |
||||
|
||||
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 <typename T> |
||||
bool operator()( const T&, const T& ) const |
||||
{ |
||||
return true; |
||||
} |
||||
}; |
||||
|
||||
template <typename T, |
||||
typename UpdatePolicy = std::not_equal_to<T>, |
||||
typename Signal = nod::unsafe_signal<void( const T& )>> |
||||
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 <typename T, typename UpdatePolicy = std::not_equal_to<T>> |
||||
using safe_property = property<T, UpdatePolicy, nod::signal<void( const T& )>>; |
||||
|
||||
} // namespace bindey
|
@ -1,681 +0,0 @@ |
||||
#ifndef IG_NOD_INCLUDE_NOD_HPP |
||||
#define IG_NOD_INCLUDE_NOD_HPP |
||||
|
||||
#include <vector> // std::vector |
||||
#include <functional> // std::function |
||||
#include <mutex> // std::mutex, std::lock_guard |
||||
#include <memory> // std::shared_ptr, std::weak_ptr |
||||
#include <algorithm> // std::find_if() |
||||
#include <cassert> // assert() |
||||
#include <thread> // std::this_thread::yield() |
||||
#include <type_traits> // std::is_same |
||||
#include <iterator> // 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 P, class T> |
||||
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<class P,class T> 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<detail::disconnector> const& shared_disconnector, std::size_t index ) : |
||||
_weak_disconnector( shared_disconnector ), |
||||
_index( index ) |
||||
{} |
||||
|
||||
/// Weak pointer to the current disconnector functor.
|
||||
std::weak_ptr<detail::disconnector> _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<connection>(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<connection>(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<mutex_type>; |
||||
/// 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 S, class T, class F, class...A> |
||||
class signal_accumulator |
||||
{ |
||||
public: |
||||
/// Result type when calling the accumulating function operator.
|
||||
using result_type = typename std::result_of<F(T, typename S::slot_type::result_type)>::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 P, class R, class... A > |
||||
class signal_type<P,R(A...)> |
||||
{ |
||||
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<disconnector*>(_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<disconnector*>(_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<R(A...)>; |
||||
/// Type that is used for counting the slots connected to this signal.
|
||||
using size_type = typename std::vector<slot_type>::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 <class T> |
||||
connection connect( T&& slot ) { |
||||
mutex_lock_type lock{ _mutex }; |
||||
_slots.push_back( std::forward<T>(slot) ); |
||||
std::size_t index = _slots.size()-1; |
||||
if( _shared_disconnector == nullptr ) { |
||||
_disconnector = disconnector{ this }; |
||||
_shared_disconnector = std::shared_ptr<detail::disconnector>{&_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 <class T, class F> |
||||
signal_accumulator<signal_type, T, F, A...> accumulate( T init, F op ) const { |
||||
static_assert( std::is_same<R,void>::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 <class C> |
||||
C aggregate( A const&... args ) const { |
||||
static_assert( std::is_same<R,void>::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<class, class, class, class...> 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<detail::disconnector> 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<slot_type> copy_slots() const |
||||
{ |
||||
mutex_lock_type lock{ _mutex }; |
||||
return _slots; |
||||
} |
||||
|
||||
/// Implementation of the signal accumulator function call
|
||||
template <class T, class F> |
||||
typename signal_accumulator<signal_type, T, F, A...>::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<P,R(A...)>* 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<P,R(A...)>* _ptr; |
||||
}; |
||||
|
||||
/// Mutex to synchronize access to the slot vector
|
||||
mutable mutex_type _mutex; |
||||
/// Vector of all connected slots
|
||||
std::vector<slot_type> _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<detail::disconnector> _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 <class T> using signal = signal_type<multithread_policy, T>; |
||||
|
||||
/// 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 <class T> using unsafe_signal = signal_type<singlethread_policy, T>; |
||||
} // namespace nod
|
||||
|
||||
#endif // IG_NOD_INCLUDE_NOD_HPP
|
@ -0,0 +1,88 @@ |
||||
#include "lua.h" |
||||
#include "luavgl.h" |
||||
#include "private.h" |
||||
#include <src/widgets/lv_slider.h> |
||||
|
||||
static int luavgl_slider_create(lua_State *L) { |
||||
return luavgl_obj_create_helper(L, lv_slider_create); |
||||
} |
||||
|
||||
static void _lv_slider_set_range(void *obj, lua_State *L) { |
||||
int min = 0, max = 100; |
||||
|
||||
int type = lua_type(L, -1); |
||||
if (type == LUA_TTABLE) { |
||||
lua_getfield(L, -1, "min"); |
||||
min = lua_tointeger(L, -1); |
||||
lua_pop(L, 1); |
||||
lua_getfield(L, -1, "max"); |
||||
max = luavgl_tointeger(L, -1); |
||||
lua_pop(L, 1); |
||||
} |
||||
|
||||
lv_slider_set_range(obj, min, max); |
||||
} |
||||
|
||||
static void _lv_slider_set_value(void *obj, int value) { |
||||
lv_slider_set_value(obj, value, LV_ANIM_OFF); |
||||
} |
||||
|
||||
static const luavgl_value_setter_t slider_property_table[] = { |
||||
{"range", SETTER_TYPE_STACK, {.setter_stack = _lv_slider_set_range}}, |
||||
{"value", SETTER_TYPE_INT, {.setter = (setter_int_t)_lv_slider_set_value}}, |
||||
}; |
||||
|
||||
LUALIB_API int luavgl_slider_set_property_kv(lua_State *L, void *data) { |
||||
lv_obj_t *obj = data; |
||||
int ret = luavgl_set_property(L, obj, slider_property_table); |
||||
|
||||
if (ret == 0) { |
||||
return 0; |
||||
} |
||||
/* a base obj property? */ |
||||
ret = luavgl_obj_set_property_kv(L, obj); |
||||
if (ret != 0) { |
||||
debug("unkown property for slider.\n"); |
||||
} |
||||
|
||||
return ret; |
||||
} |
||||
|
||||
static int luavgl_slider_set(lua_State *L) { |
||||
lv_obj_t *obj = luavgl_to_obj(L, 1); |
||||
|
||||
if (!lua_istable(L, -1)) { |
||||
luaL_error(L, "expect a table on 2nd para."); |
||||
return 0; |
||||
} |
||||
|
||||
luavgl_iterate(L, -1, luavgl_slider_set_property_kv, obj); |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
static int luavgl_slider_value(lua_State *L) { |
||||
lv_obj_t *obj = luavgl_to_obj(L, 1); |
||||
lua_pushinteger(L, lv_slider_get_value(obj)); |
||||
return 1; |
||||
} |
||||
|
||||
static int luavgl_slider_tostring(lua_State *L) { |
||||
lv_obj_t *obj = luavgl_to_obj(L, 1); |
||||
lua_pushfstring(L, "lv_slider:%p", obj); |
||||
return 1; |
||||
} |
||||
|
||||
static const luaL_Reg luavgl_slider_methods[] = { |
||||
{"set", luavgl_slider_set}, |
||||
{"value", luavgl_slider_value}, |
||||
{NULL, NULL}, |
||||
}; |
||||
|
||||
static void luavgl_slider_init(lua_State *L) { |
||||
luavgl_obj_newmetatable(L, &lv_slider_class, "lv_slider", |
||||
luavgl_slider_methods); |
||||
lua_pushcfunction(L, luavgl_slider_tostring); |
||||
lua_setfield(L, -2, "__tostring"); |
||||
lua_pop(L, 1); |
||||
} |
@ -0,0 +1,57 @@ |
||||
#include "luavgl.h" |
||||
#include "private.h" |
||||
#include <src/widgets/lv_switch.h> |
||||
|
||||
static int luavgl_switch_create(lua_State *L) { |
||||
return luavgl_obj_create_helper(L, lv_switch_create); |
||||
} |
||||
|
||||
LUALIB_API int luavgl_switch_set_property_kv(lua_State *L, void *data) { |
||||
lv_obj_t *obj = data; |
||||
/* switches only use base properties */ |
||||
int ret = luavgl_obj_set_property_kv(L, obj); |
||||
if (ret != 0) { |
||||
debug("unkown property for switch.\n"); |
||||
} |
||||
|
||||
return ret; |
||||
} |
||||
|
||||
static int luavgl_switch_set(lua_State *L) { |
||||
lv_obj_t *obj = luavgl_to_obj(L, 1); |
||||
|
||||
if (!lua_istable(L, -1)) { |
||||
luaL_error(L, "expect a table on 2nd para."); |
||||
return 0; |
||||
} |
||||
|
||||
luavgl_iterate(L, -1, luavgl_switch_set_property_kv, obj); |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
static int luavgl_switch_enabled(lua_State *L) { |
||||
lv_obj_t *obj = luavgl_to_obj(L, 1); |
||||
lua_pushboolean(L, lv_obj_has_state(obj, LV_STATE_CHECKED)); |
||||
return 1; |
||||
} |
||||
|
||||
static int luavgl_switch_tostring(lua_State *L) { |
||||
lv_obj_t *obj = luavgl_to_obj(L, 1); |
||||
lua_pushfstring(L, "lv_switch:%p", obj); |
||||
return 1; |
||||
} |
||||
|
||||
static const luaL_Reg luavgl_switch_methods[] = { |
||||
{"set", luavgl_switch_set}, |
||||
{"enabled", luavgl_switch_enabled}, |
||||
{NULL, NULL}, |
||||
}; |
||||
|
||||
static void luavgl_switch_init(lua_State *L) { |
||||
luavgl_obj_newmetatable(L, &lv_switch_class, "lv_switch", |
||||
luavgl_switch_methods); |
||||
lua_pushcfunction(L, luavgl_switch_tostring); |
||||
lua_setfield(L, -2, "__tostring"); |
||||
lua_pop(L, 1); |
||||
} |
@ -0,0 +1,189 @@ |
||||
local backstack = require("backstack") |
||||
local widgets = require("widgets") |
||||
local font = require("font") |
||||
local theme = require("theme") |
||||
|
||||
local function show_license(text) |
||||
backstack.push(function() |
||||
local screen = widgets.MenuScreen { |
||||
show_back = true, |
||||
title = "Licenses", |
||||
} |
||||
screen.root:Label { |
||||
w = lvgl.PCT(100), |
||||
h = lvgl.SIZE_CONTENT, |
||||
text_font = font.fusion_10, |
||||
text = text, |
||||
} |
||||
end) |
||||
end |
||||
|
||||
local function gpl(copyright) |
||||
show_license(copyright .. [[ |
||||
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. |
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
||||
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.]]) |
||||
end |
||||
|
||||
local function faadgpl() |
||||
show_license( |
||||
[[FAAD2 - Freeware Advanced Audio (AAC) Decoder including SBR decoding Copyright (C) 2003-2005 M. Bakker, Nero AG, http://www.nero.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. |
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
||||
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. Any non-GPL usage of this software or parts of this software is strictly forbidden. |
||||
The "appropriate copyright message" mentioned in section 2c of the GPLv2 must read: "Code from FAAD2 is copyright (c) Nero AG, www.nero.com" Commercial non-GPL licensing of this software is possible. For more info contact Nero AG through Mpeg4AAClicense@nero.com.]]) |
||||
end |
||||
|
||||
local function bsd(copyright) |
||||
show_license(copyright .. [[ |
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. |
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.]]) |
||||
end |
||||
|
||||
local function xiphbsd(copyright) |
||||
show_license(copyright .. [[ |
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: |
||||
|
||||
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. |
||||
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. |
||||
- Neither the name of the Xiph.org Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``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 FOUNDATION OR CONTRIBUTORS 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.]]) |
||||
end |
||||
|
||||
local function apache(copyright) |
||||
show_license(copyright .. [[ |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at |
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.]]) |
||||
end |
||||
|
||||
local function mit(copyright) |
||||
show_license(copyright .. [[ |
||||
|
||||
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.]]) |
||||
end |
||||
|
||||
local function boost(copyright) |
||||
show_license(copyright .. [[ |
||||
|
||||
Boost Software License - Version 1.0 - August 17th, 2003 |
||||
Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: |
||||
The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. |
||||
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, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.]]) |
||||
end |
||||
|
||||
|
||||
return function() |
||||
local menu = widgets.MenuScreen(true, "Licenses") |
||||
|
||||
local container = menu.root:Object { |
||||
flex = { |
||||
flex_direction = "column", |
||||
flex_wrap = "nowrap", |
||||
justify_content = "flex-start", |
||||
align_items = "flex-start", |
||||
align_content = "flex-start", |
||||
}, |
||||
w = lvgl.PCT(100), |
||||
h = lvgl.SIZE_CONTENT, |
||||
} |
||||
|
||||
local function library(name, license, show_fn) |
||||
local row = container:Object { |
||||
flex = { |
||||
flex_direction = "row", |
||||
justify_content = "flex-start", |
||||
align_items = "flex-start", |
||||
align_content = "flex-start", |
||||
}, |
||||
w = lvgl.PCT(100), |
||||
h = lvgl.SIZE_CONTENT, |
||||
} |
||||
row:add_style(theme.list_item) |
||||
row:Label { text = name, flex_grow = 1 } |
||||
local button = row:Button {} |
||||
button:Label { text = license, text_font = font.fusion_10 } |
||||
button:onClicked(show_fn) |
||||
end |
||||
|
||||
library("ESP-IDF", "Apache 2.0", function() |
||||
apache("2015-2024 Espressif Systems (Shanghai) CO LTD") |
||||
end) |
||||
library("esp-idf-lua", "MIT", function() |
||||
mit("Copyright (C) 2019 Ruslan V. Uss") |
||||
end) |
||||
library("FAAD2", "GPL", function() |
||||
faadgpl() |
||||
end) |
||||
library("FatFs", "BSD", function() |
||||
bsd("Copyright (C) 2022, ChaN, all right reserved.") |
||||
end) |
||||
library("komihash", "MIT", function() |
||||
mit("Copyright (c) 2021-2022 Aleksey Vaneev") |
||||
end) |
||||
library("LevelDB", "BSD", function() |
||||
bsd("Copyright (c) 2011 The LevelDB Authors. All rights reserved.") |
||||
end) |
||||
library("libcppbor", "Apache 2.0", function() |
||||
apache("Copyright 2019 Google LLC") |
||||
end) |
||||
library("libmad", "GPL", function() |
||||
gpl("Copyright (C) 2000-2004 Underbit Technologies, Inc.") |
||||
end) |
||||
library("libtags", "MIT", function() |
||||
mit("Copyright © 2013-2020 Sigrid Solveig Haflínudóttir") |
||||
end) |
||||
library("Lua", "MIT", function() |
||||
mit("Copyright (C) 1994-2018 Lua.org, PUC-Rio") |
||||
end) |
||||
library("lua-linenoise", "MIT", function() |
||||
mit("Copyright (c) 2011-2015 Rob Hoelz <rob@hoelz.ro>") |
||||
end) |
||||
library("lua-term", "MIT", function() |
||||
mit("Copyright (c) 2009 Rob Hoelz <rob@hoelzro.net>") |
||||
end) |
||||
library("luavgl", "MIT", function() |
||||
mit("Copyright (c) 2022 Neo Xu") |
||||
end) |
||||
library("LVGL", "MIT", function() |
||||
mit("Copyright (c) 2021 LVGL Kft") |
||||
end) |
||||
library("MillerShuffle", "Apache 2.0", function() |
||||
apache("Copyright 2022 Ronald Ross Miller") |
||||
end) |
||||
library("miniflac", "BSD", function() |
||||
bsd("Copyright (C) 2022 John Regan <john@jrjrtech.com>") |
||||
end) |
||||
library("ogg", "BSD", function() |
||||
xiphbsd("Copyright (c) 2002, Xiph.org Foundation") |
||||
end) |
||||
library("Opus", "BSD", function() |
||||
xiphbsd( |
||||
"Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo") |
||||
end) |
||||
library("Opusfile", "BSD", function() |
||||
xiphbsd("Copyright (c) 1994-2013 Xiph.Org Foundation and contributors") |
||||
end) |
||||
library("result", "MIT", function() |
||||
mit("Copyright (c) 2017-2021 Matthew Rodusek") |
||||
end) |
||||
library("span", "Boost", function() |
||||
boost("Copyright Tristan Brindle 2018") |
||||
end) |
||||
library("speexdsp", "bsd", function() |
||||
xiphbsd( |
||||
"Copyright 2002-2008 Xiph.org Foundation, Copyright 2002-2008 Jean-Marc Valin, Copyright 2005-2007 Analog Devices Inc., Copyright 2005-2008 Commonwealth Scientific and Industrial Research, Organisation (CSIRO), Copyright 1993, 2002, 2006 David Rowe, Copyright 2003 EpicGames, Copyright 1992-1994 Jutta Degener, Carsten Bormann") |
||||
end) |
||||
library("tinyfsm", "MIT", function() |
||||
mit("Copyright (c) 2012-2022 Axel Burri") |
||||
end) |
||||
library("tremor", "bsd", function() |
||||
xiphbsd("Copyright (c) 2002, Xiph.org Foundation") |
||||
end) |
||||
end |
@ -0,0 +1,249 @@ |
||||
local lvgl = require("lvgl") |
||||
local backstack = require("backstack") |
||||
local widgets = require("widgets") |
||||
local theme = require("theme") |
||||
local volume = require("volume") |
||||
|
||||
local settings = {} |
||||
|
||||
local function SettingsScreen(title) |
||||
local menu = widgets.MenuScreen { |
||||
show_back = true, |
||||
title = title, |
||||
} |
||||
menu.content = menu.root:Object { |
||||
flex = { |
||||
flex_direction = "column", |
||||
flex_wrap = "nowrap", |
||||
justify_content = "flex-start", |
||||
align_items = "flex-start", |
||||
align_content = "flex-start", |
||||
}, |
||||
w = lvgl.PCT(100), |
||||
flex_grow = 1, |
||||
pad_left = 4, |
||||
pad_right = 4, |
||||
} |
||||
return menu |
||||
end |
||||
|
||||
function settings.bluetooth() |
||||
local menu = SettingsScreen("Bluetooth") |
||||
|
||||
local enable_container = menu.content:Object { |
||||
flex = { |
||||
flex_direction = "row", |
||||
justify_content = "flex-start", |
||||
align_items = "flex-start", |
||||
align_content = "flex-start", |
||||
}, |
||||
w = lvgl.PCT(100), |
||||
h = lvgl.SIZE_CONTENT, |
||||
} |
||||
enable_container:Label { text = "Enable", flex_grow = 1 } |
||||
enable_container:Switch {} |
||||
|
||||
local preferred_container = menu.content:Object { |
||||
flex = { |
||||
flex_direction = "row", |
||||
justify_content = "flex-start", |
||||
align_items = "flex-start", |
||||
align_content = "flex-start", |
||||
}, |
||||
w = lvgl.PCT(100), |
||||
h = lvgl.SIZE_CONTENT, |
||||
} |
||||
preferred_container:add_style(theme.settings_title) |
||||
preferred_container:Label { |
||||
text = "Preferred Device", |
||||
flex_grow = 1, |
||||
} |
||||
preferred_container:Label { text = "x" } |
||||
|
||||
local preferred_device = menu.content:Label { |
||||
text = "My Cool Speakers", |
||||
} |
||||
|
||||
menu.content:Label { |
||||
text = "Available Devices", |
||||
}:add_style(theme.settings_title) |
||||
|
||||
local known_devices = menu.content:List { |
||||
w = lvgl.PCT(100), |
||||
flex_grow = 1, |
||||
} |
||||
end |
||||
|
||||
function settings.headphones() |
||||
local menu = SettingsScreen("Headphones") |
||||
|
||||
menu.content:Label { |
||||
text = "Maximum volume limit", |
||||
}:add_style(theme.settings_title) |
||||
|
||||
local volume_chooser = menu.content:Dropdown { |
||||
options = "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)", |
||||
selected = 1, |
||||
} |
||||
local limits = { -10, 6, 10 } |
||||
volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() |
||||
local selection = volume_chooser:get('selected') |
||||
volume.limit_db.set(limits[selection]) |
||||
end) |
||||
|
||||
menu.content:Label { |
||||
text = "Left/Right balance", |
||||
}:add_style(theme.settings_title) |
||||
|
||||
local balance = menu.content:Slider { |
||||
w = lvgl.PCT(100), |
||||
h = 5, |
||||
range = { min = -5, max = 5 }, |
||||
value = 0, |
||||
} |
||||
balance:onevent(lvgl.EVENT.VALUE_CHANGED, function() |
||||
volume.left_bias:set(balance:value()) |
||||
end) |
||||
|
||||
local balance_label = menu.content:Label {} |
||||
|
||||
menu.bindings = { |
||||
volume.limit_db:bind(function(limit) |
||||
print("new limit", limit) |
||||
for i=1,#limits do |
||||
if limits[i] == limit then |
||||
volume_chooser:set{ selected = i } |
||||
end |
||||
end |
||||
end), |
||||
volume.left_bias:bind(function(bias) |
||||
balance:set { |
||||
value = bias |
||||
} |
||||
if bias < 0 then |
||||
balance_label:set { |
||||
text = string.format("Left -%.2fdB", bias / 4) |
||||
} |
||||
else |
||||
balance_label:set { |
||||
text = string.format("Right -%.2fdB", -bias / 4) |
||||
} |
||||
end |
||||
end), |
||||
} |
||||
|
||||
return menu |
||||
end |
||||
|
||||
function settings.display() |
||||
local menu = SettingsScreen("Display") |
||||
|
||||
menu.content:Label { |
||||
text = "Brightness", |
||||
}:add_style(theme.settings_title) |
||||
|
||||
local brightness = menu.content:Slider { |
||||
w = lvgl.PCT(100), |
||||
h = 5, |
||||
range = { min = 0, max = 100 }, |
||||
value = 50, |
||||
} |
||||
brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function() |
||||
print("bright", brightness:value()) |
||||
end) |
||||
end |
||||
|
||||
function settings.input() |
||||
local menu = SettingsScreen("Input Method") |
||||
|
||||
menu.content:Label { |
||||
text = "Control scheme", |
||||
}:add_style(theme.settings_title) |
||||
|
||||
local controls_chooser = menu.content:Dropdown { |
||||
options = "Buttons only\nD-Pad\nTouchwheel", |
||||
selected = 3, |
||||
} |
||||
controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() |
||||
local selection = controls_chooser:get('selected') |
||||
print("controls", selection) |
||||
end) |
||||
end |
||||
|
||||
function settings.database() |
||||
local menu = SettingsScreen("Database") |
||||
local db = require("database") |
||||
widgets.Row(menu.content, "Schema version", db.version()) |
||||
widgets.Row(menu.content, "Size on disk", string.format("%.1f KiB", db.size() / 1024)) |
||||
|
||||
local actions_container = menu.content:Object { |
||||
w = lvgl.PCT(100), |
||||
h = lvgl.SIZE_CONTENT, |
||||
flex = { |
||||
flex_direction = "row", |
||||
justify_content = "center", |
||||
align_items = "space-evenly", |
||||
align_content = "center", |
||||
}, |
||||
pad_top = 4, |
||||
pad_column = 4, |
||||
} |
||||
actions_container:add_style(theme.list_item) |
||||
|
||||
local update = actions_container:Button {} |
||||
update:Label { text = "Update" } |
||||
|
||||
local recreate = actions_container:Button {} |
||||
recreate:Label { text = "Recreate" } |
||||
end |
||||
|
||||
function settings.firmware() |
||||
local menu = SettingsScreen("Firmware") |
||||
local version = require("version") |
||||
widgets.Row(menu.content, "ESP32", version.esp()) |
||||
widgets.Row(menu.content, "SAMD21", version.samd()) |
||||
widgets.Row(menu.content, "Collator", version.collator()) |
||||
end |
||||
|
||||
function settings.root() |
||||
local menu = widgets.MenuScreen { |
||||
show_back = true, |
||||
title = "Settings", |
||||
} |
||||
menu.list = menu.root:List { |
||||
w = lvgl.PCT(100), |
||||
h = lvgl.PCT(100), |
||||
flex_grow = 1, |
||||
} |
||||
|
||||
local function section(name) |
||||
menu.list:add_text(name):add_style(theme.list_heading) |
||||
end |
||||
|
||||
local function submenu(name, fn) |
||||
local item = menu.list:add_btn(nil, name) |
||||
item:onClicked(function() |
||||
backstack.push(fn) |
||||
end) |
||||
item:add_style(theme.list_item) |
||||
end |
||||
|
||||
section("Audio") |
||||
submenu("Bluetooth", settings.bluetooth) |
||||
submenu("Headphones", settings.headphones) |
||||
|
||||
section("Interface") |
||||
submenu("Display", settings.display) |
||||
submenu("Input Method", settings.input) |
||||
|
||||
section("System") |
||||
submenu("Database", settings.database) |
||||
submenu("Firmware", settings.firmware) |
||||
submenu("Licenses", function() |
||||
return require("licenses")() |
||||
end) |
||||
|
||||
return menu |
||||
end |
||||
|
||||
return settings |
@ -1,10 +1,23 @@ |
||||
local lvgl = require("lvgl") |
||||
local font = require("font") |
||||
|
||||
local theme = { |
||||
list_item = lvgl.Style { |
||||
pad_left = 4, |
||||
pad_right = 4, |
||||
}, |
||||
list_heading = lvgl.Style { |
||||
pad_top = 4, |
||||
pad_left = 4, |
||||
pad_right = 4, |
||||
text_font = font.fusion_10, |
||||
text_align = lvgl.ALIGN.CENTER, |
||||
}, |
||||
settings_title = lvgl.Style { |
||||
pad_top = 2, |
||||
pad_bottom = 4, |
||||
text_font = font.fusion_10, |
||||
} |
||||
} |
||||
|
||||
return theme |
||||
|
@ -1,23 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* 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<EventBinding*>(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
|
@ -1,30 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <cstdint> |
||||
|
||||
#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<void(lv_obj_t*)>& { return signal_; } |
||||
|
||||
private: |
||||
lv_obj_t* obj_; |
||||
nod::signal<void(lv_obj_t*)> signal_; |
||||
}; |
||||
|
||||
} // namespace ui
|
@ -1,29 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <memory> |
||||
#include <vector> |
||||
|
||||
#include "index.hpp" |
||||
#include "lvgl.h" |
||||
|
||||
#include "modal.hpp" |
||||
|
||||
namespace ui { |
||||
namespace modals { |
||||
|
||||
class Confirm : public Modal { |
||||
public: |
||||
Confirm(Screen*, const std::pmr::string& title, bool has_cancel); |
||||
|
||||
private: |
||||
lv_obj_t* container_; |
||||
}; |
||||
|
||||
} // namespace modals
|
||||
} // namespace ui
|
@ -1,34 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <memory> |
||||
#include <vector> |
||||
|
||||
#include "index.hpp" |
||||
#include "lvgl.h" |
||||
|
||||
#include "modal.hpp" |
||||
|
||||
namespace ui { |
||||
namespace modals { |
||||
|
||||
class Progress : public Modal { |
||||
public: |
||||
Progress(Screen*, std::pmr::string title, std::pmr::string subtitle = ""); |
||||
|
||||
void title(const std::pmr::string&); |
||||
void subtitle(const std::pmr::string&); |
||||
|
||||
private: |
||||
lv_obj_t* container_; |
||||
lv_obj_t* title_; |
||||
lv_obj_t* subtitle_; |
||||
}; |
||||
|
||||
} // namespace modals
|
||||
} // namespace ui
|
@ -1,26 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include "bindey/property.h" |
||||
|
||||
#include "track.hpp" |
||||
|
||||
namespace ui { |
||||
namespace models { |
||||
|
||||
struct Playback { |
||||
bindey::property<bool> is_playing; |
||||
bindey::property<std::optional<database::TrackId>> current_track; |
||||
bindey::property<std::vector<database::TrackId>> upcoming_tracks; |
||||
|
||||
bindey::property<uint32_t> current_track_position; |
||||
bindey::property<uint32_t> current_track_duration; |
||||
}; |
||||
|
||||
} // namespace models
|
||||
} // namespace ui
|
@ -1,26 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include "battery.hpp" |
||||
#include "bindey/property.h" |
||||
|
||||
#include "track.hpp" |
||||
|
||||
namespace ui { |
||||
namespace models { |
||||
|
||||
struct TopBar { |
||||
bindey::property<battery::Battery::BatteryState> battery_state; |
||||
|
||||
// Shared with the Playback model
|
||||
bindey::property<bool>& is_playing; |
||||
bindey::property<std::optional<database::TrackId>>& current_track; |
||||
}; |
||||
|
||||
} // namespace models
|
||||
} // namespace ui
|
@ -1,116 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <stdint.h> |
||||
#include <cstdint> |
||||
#include <list> |
||||
#include <memory> |
||||
#include <vector> |
||||
|
||||
#include "bluetooth.hpp" |
||||
#include "bluetooth_types.hpp" |
||||
#include "display.hpp" |
||||
#include "index.hpp" |
||||
#include "lvgl.h" |
||||
|
||||
#include "model_top_bar.hpp" |
||||
#include "nvs.hpp" |
||||
#include "samd.hpp" |
||||
#include "screen.hpp" |
||||
|
||||
namespace ui { |
||||
namespace screens { |
||||
|
||||
class Settings : public MenuScreen { |
||||
public: |
||||
Settings(models::TopBar&); |
||||
}; |
||||
|
||||
class Bluetooth : public MenuScreen { |
||||
public: |
||||
Bluetooth(models::TopBar&, drivers::Bluetooth& bt, drivers::NvsStorage& nvs); |
||||
~Bluetooth(); |
||||
|
||||
auto ChangeEnabledState(bool enabled) -> void; |
||||
auto RefreshDevicesList() -> void; |
||||
auto OnDeviceSelected(ssize_t index) -> void; |
||||
|
||||
private: |
||||
auto RemoveAllDevices() -> void; |
||||
auto AddPreferredDevice(const drivers::bluetooth::Device&) -> void; |
||||
auto AddDevice(const drivers::bluetooth::Device&) -> void; |
||||
|
||||
drivers::Bluetooth& bt_; |
||||
drivers::NvsStorage& nvs_; |
||||
|
||||
lv_obj_t* devices_list_; |
||||
lv_obj_t* preferred_device_; |
||||
|
||||
std::list<drivers::bluetooth::mac_addr_t> macs_in_list_; |
||||
}; |
||||
|
||||
class Headphones : public MenuScreen { |
||||
public: |
||||
Headphones(models::TopBar&, drivers::NvsStorage& nvs); |
||||
|
||||
auto ChangeMaxVolume(uint8_t index) -> void; |
||||
auto ChangeCustomVolume(int8_t diff) -> void; |
||||
|
||||
private: |
||||
auto UpdateCustomVol(uint16_t) -> void; |
||||
|
||||
drivers::NvsStorage& nvs_; |
||||
lv_obj_t* custom_vol_container_; |
||||
lv_obj_t* custom_vol_label_; |
||||
|
||||
std::vector<uint16_t> index_to_level_; |
||||
uint16_t custom_limit_; |
||||
}; |
||||
|
||||
class Appearance : public MenuScreen { |
||||
public: |
||||
Appearance(models::TopBar&, |
||||
drivers::NvsStorage& nvs, |
||||
drivers::Display& display); |
||||
|
||||
auto ChangeBrightness(uint_fast8_t) -> void; |
||||
auto CommitBrightness() -> void; |
||||
|
||||
private: |
||||
drivers::NvsStorage& nvs_; |
||||
drivers::Display& display_; |
||||
|
||||
lv_obj_t* current_brightness_label_; |
||||
uint_fast8_t current_brightness_; |
||||
}; |
||||
|
||||
class InputMethod : public MenuScreen { |
||||
public: |
||||
InputMethod(models::TopBar&, drivers::NvsStorage& nvs); |
||||
|
||||
private: |
||||
drivers::NvsStorage& nvs_; |
||||
}; |
||||
|
||||
class Storage : public MenuScreen { |
||||
public: |
||||
Storage(models::TopBar&); |
||||
}; |
||||
|
||||
class FirmwareUpdate : public MenuScreen { |
||||
public: |
||||
FirmwareUpdate(models::TopBar&, drivers::Samd&); |
||||
}; |
||||
|
||||
class About : public MenuScreen { |
||||
public: |
||||
About(models::TopBar&); |
||||
}; |
||||
|
||||
} // namespace screens
|
||||
} // namespace ui
|
@ -1,45 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <cstdint> |
||||
#include <string> |
||||
|
||||
#include "bindey/binding.h" |
||||
#include "lvgl.h" |
||||
|
||||
#include "memory_resource.hpp" |
||||
#include "model_top_bar.hpp" |
||||
|
||||
namespace ui { |
||||
|
||||
namespace widgets { |
||||
|
||||
class TopBar { |
||||
public: |
||||
struct Configuration { |
||||
bool show_back_button; |
||||
std::pmr::string title; |
||||
}; |
||||
|
||||
explicit TopBar(lv_obj_t* parent, |
||||
const Configuration& config, |
||||
models::TopBar& model); |
||||
|
||||
auto root() -> lv_obj_t* { return container_; } |
||||
auto button() -> lv_obj_t* { return back_button_; } |
||||
|
||||
private: |
||||
std::vector<bindey::scoped_binding> bindings_; |
||||
|
||||
lv_obj_t* container_; |
||||
lv_obj_t* back_button_; |
||||
}; |
||||
|
||||
} // namespace widgets
|
||||
|
||||
} // namespace ui
|
@ -1,76 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#include "modal_confirm.hpp" |
||||
|
||||
#include "core/lv_event.h" |
||||
#include "core/lv_obj.h" |
||||
#include "core/lv_obj_tree.h" |
||||
#include "esp_log.h" |
||||
|
||||
#include "core/lv_group.h" |
||||
#include "core/lv_obj_pos.h" |
||||
#include "event_queue.hpp" |
||||
#include "extra/widgets/list/lv_list.h" |
||||
#include "extra/widgets/menu/lv_menu.h" |
||||
#include "extra/widgets/spinner/lv_spinner.h" |
||||
#include "hal/lv_hal_disp.h" |
||||
#include "index.hpp" |
||||
#include "misc/lv_area.h" |
||||
#include "ui_events.hpp" |
||||
#include "ui_fsm.hpp" |
||||
#include "widget_top_bar.hpp" |
||||
#include "widgets/lv_btn.h" |
||||
#include "widgets/lv_label.h" |
||||
|
||||
namespace ui { |
||||
namespace modals { |
||||
|
||||
static void button_cancel_cb(lv_event_t* e) { |
||||
events::Ui().Dispatch(internal::ModalCancelPressed{}); |
||||
} |
||||
|
||||
static void button_confirm_cb(lv_event_t* e) { |
||||
events::Ui().Dispatch(internal::ModalConfirmPressed{}); |
||||
} |
||||
|
||||
Confirm::Confirm(Screen* host, |
||||
const std::pmr::string& title_text, |
||||
bool has_cancel) |
||||
: Modal(host) { |
||||
lv_obj_set_layout(root_, LV_LAYOUT_FLEX); |
||||
lv_obj_set_flex_flow(root_, LV_FLEX_FLOW_COLUMN); |
||||
lv_obj_set_flex_align(root_, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, |
||||
LV_FLEX_ALIGN_CENTER); |
||||
|
||||
lv_obj_t* title = lv_label_create(root_); |
||||
lv_label_set_text(title, title_text.c_str()); |
||||
lv_obj_set_size(title, LV_SIZE_CONTENT, LV_SIZE_CONTENT); |
||||
|
||||
lv_obj_t* button_container = lv_obj_create(root_); |
||||
lv_obj_set_size(button_container, lv_pct(100), LV_SIZE_CONTENT); |
||||
lv_obj_set_layout(button_container, LV_LAYOUT_FLEX); |
||||
lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW); |
||||
lv_obj_set_flex_align(button_container, LV_FLEX_ALIGN_SPACE_EVENLY, |
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); |
||||
|
||||
if (has_cancel) { |
||||
lv_obj_t* cancel_btn = lv_btn_create(button_container); |
||||
lv_obj_t* cancel_label = lv_label_create(cancel_btn); |
||||
lv_label_set_text(cancel_label, "Cancel"); |
||||
lv_group_add_obj(group_, cancel_btn); |
||||
lv_obj_add_event_cb(cancel_btn, button_cancel_cb, LV_EVENT_CLICKED, NULL); |
||||
} |
||||
|
||||
lv_obj_t* ok_btn = lv_btn_create(button_container); |
||||
lv_obj_t* ok_label = lv_label_create(ok_btn); |
||||
lv_label_set_text(ok_label, "Okay"); |
||||
lv_group_add_obj(group_, ok_btn); |
||||
lv_obj_add_event_cb(ok_btn, button_confirm_cb, LV_EVENT_CLICKED, NULL); |
||||
} |
||||
|
||||
} // namespace modals
|
||||
} // namespace ui
|
@ -1,67 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#include "modal_progress.hpp" |
||||
|
||||
#include "core/lv_event.h" |
||||
#include "core/lv_obj.h" |
||||
#include "core/lv_obj_tree.h" |
||||
#include "esp_log.h" |
||||
|
||||
#include "core/lv_group.h" |
||||
#include "core/lv_obj_pos.h" |
||||
#include "event_queue.hpp" |
||||
#include "extra/widgets/list/lv_list.h" |
||||
#include "extra/widgets/menu/lv_menu.h" |
||||
#include "extra/widgets/spinner/lv_spinner.h" |
||||
#include "hal/lv_hal_disp.h" |
||||
#include "index.hpp" |
||||
#include "misc/lv_area.h" |
||||
#include "ui_events.hpp" |
||||
#include "ui_fsm.hpp" |
||||
#include "widget_top_bar.hpp" |
||||
#include "widgets/lv_label.h" |
||||
|
||||
namespace ui { |
||||
namespace modals { |
||||
|
||||
Progress::Progress(Screen* host, |
||||
std::pmr::string title_text, |
||||
std::pmr::string subtitle_text) |
||||
: Modal(host) { |
||||
lv_obj_set_layout(root_, LV_LAYOUT_FLEX); |
||||
lv_obj_set_flex_flow(root_, LV_FLEX_FLOW_COLUMN); |
||||
lv_obj_set_flex_align(root_, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, |
||||
LV_FLEX_ALIGN_CENTER); |
||||
|
||||
title_ = lv_label_create(root_); |
||||
lv_obj_set_size(title_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); |
||||
|
||||
subtitle_ = lv_label_create(root_); |
||||
lv_obj_set_size(subtitle_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); |
||||
|
||||
lv_obj_t* spinner = lv_spinner_create(root_, 3000, 45); |
||||
lv_obj_set_size(spinner, 16, 16); |
||||
|
||||
title(title_text); |
||||
subtitle(subtitle_text); |
||||
} |
||||
|
||||
void Progress::title(const std::pmr::string& s) { |
||||
lv_label_set_text(title_, s.c_str()); |
||||
} |
||||
|
||||
void Progress::subtitle(const std::pmr::string& s) { |
||||
if (s.empty()) { |
||||
lv_obj_add_flag(subtitle_, LV_OBJ_FLAG_HIDDEN); |
||||
} else { |
||||
lv_obj_clear_flag(subtitle_, LV_OBJ_FLAG_HIDDEN); |
||||
lv_label_set_text(subtitle_, s.c_str()); |
||||
} |
||||
} |
||||
|
||||
} // namespace modals
|
||||
} // namespace ui
|
@ -1,575 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#include "screen_settings.hpp" |
||||
#include <stdint.h> |
||||
#include <string> |
||||
|
||||
#include "audio_events.hpp" |
||||
#include "bluetooth.hpp" |
||||
#include "bluetooth_types.hpp" |
||||
#include "core/lv_event.h" |
||||
#include "core/lv_obj.h" |
||||
#include "core/lv_obj_tree.h" |
||||
#include "display.hpp" |
||||
#include "esp_app_desc.h" |
||||
#include "esp_log.h" |
||||
|
||||
#include "core/lv_group.h" |
||||
#include "core/lv_obj_pos.h" |
||||
#include "event_queue.hpp" |
||||
#include "extra/layouts/flex/lv_flex.h" |
||||
#include "extra/widgets/list/lv_list.h" |
||||
#include "extra/widgets/menu/lv_menu.h" |
||||
#include "extra/widgets/spinbox/lv_spinbox.h" |
||||
#include "extra/widgets/spinner/lv_spinner.h" |
||||
#include "hal/lv_hal_disp.h" |
||||
#include "index.hpp" |
||||
#include "lv_api_map.h" |
||||
#include "misc/lv_anim.h" |
||||
#include "misc/lv_area.h" |
||||
#include "model_top_bar.hpp" |
||||
#include "nvs.hpp" |
||||
#include "samd.hpp" |
||||
#include "screen.hpp" |
||||
#include "themes.hpp" |
||||
#include "ui_events.hpp" |
||||
#include "ui_fsm.hpp" |
||||
#include "widget_top_bar.hpp" |
||||
#include "widgets/lv_bar.h" |
||||
#include "widgets/lv_btn.h" |
||||
#include "widgets/lv_dropdown.h" |
||||
#include "widgets/lv_label.h" |
||||
#include "widgets/lv_slider.h" |
||||
#include "widgets/lv_switch.h" |
||||
#include "wm8523.hpp" |
||||
|
||||
namespace ui { |
||||
namespace screens { |
||||
|
||||
using Page = internal::ShowSettingsPage::Page; |
||||
|
||||
static void open_sub_menu_cb(lv_event_t* e) { |
||||
Page next_page = static_cast<Page>(reinterpret_cast<uintptr_t>(e->user_data)); |
||||
events::Ui().Dispatch(internal::ShowSettingsPage{ |
||||
.page = next_page, |
||||
}); |
||||
} |
||||
|
||||
static void sub_menu(lv_obj_t* list, |
||||
lv_group_t* group, |
||||
const std::pmr::string& text, |
||||
Page page) { |
||||
lv_obj_t* item = lv_list_add_btn(list, NULL, text.c_str()); |
||||
lv_group_add_obj(group, item); |
||||
lv_obj_add_event_cb(item, open_sub_menu_cb, LV_EVENT_CLICKED, |
||||
reinterpret_cast<void*>(static_cast<uintptr_t>(page))); |
||||
} |
||||
|
||||
Settings::Settings(models::TopBar& bar) : MenuScreen(bar, "Settings") { |
||||
lv_obj_t* list = lv_list_create(content_); |
||||
lv_obj_set_size(list, lv_pct(100), lv_pct(100)); |
||||
|
||||
themes::Theme::instance()->ApplyStyle(lv_list_add_text(list, "Audio"), |
||||
themes::Style::kMenuSubheadFirst); |
||||
sub_menu(list, group_, "Bluetooth", Page::kBluetooth); |
||||
sub_menu(list, group_, "Headphones", Page::kHeadphones); |
||||
|
||||
themes::Theme::instance()->ApplyStyle(lv_list_add_text(list, "Interface"), |
||||
themes::Style::kMenuSubhead); |
||||
sub_menu(list, group_, "Appearance", Page::kAppearance); |
||||
sub_menu(list, group_, "Input Method", Page::kInput); |
||||
|
||||
themes::Theme::instance()->ApplyStyle(lv_list_add_text(list, "System"), |
||||
themes::Style::kMenuSubhead); |
||||
sub_menu(list, group_, "Storage", Page::kStorage); |
||||
sub_menu(list, group_, "Firmware Update", Page::kFirmwareUpdate); |
||||
sub_menu(list, group_, "About", Page::kAbout); |
||||
} |
||||
|
||||
static auto settings_container(lv_obj_t* parent) -> lv_obj_t* { |
||||
lv_obj_t* res = lv_obj_create(parent); |
||||
lv_obj_set_layout(res, LV_LAYOUT_FLEX); |
||||
lv_obj_set_size(res, lv_pct(100), LV_SIZE_CONTENT); |
||||
lv_obj_set_flex_flow(res, LV_FLEX_FLOW_ROW); |
||||
lv_obj_set_flex_align(res, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, |
||||
LV_FLEX_ALIGN_START); |
||||
return res; |
||||
} |
||||
|
||||
static auto label_pair(lv_obj_t* parent, |
||||
const std::pmr::string& left, |
||||
const std::pmr::string& right) -> lv_obj_t* { |
||||
lv_obj_t* container = settings_container(parent); |
||||
lv_obj_t* left_label = lv_label_create(container); |
||||
lv_label_set_text(left_label, left.c_str()); |
||||
lv_obj_t* right_label = lv_label_create(container); |
||||
lv_label_set_text(right_label, right.c_str()); |
||||
return right_label; |
||||
} |
||||
|
||||
static auto toggle_bt_cb(lv_event_t* ev) { |
||||
Bluetooth* instance = reinterpret_cast<Bluetooth*>(ev->user_data); |
||||
instance->ChangeEnabledState(lv_obj_has_state(ev->target, LV_STATE_CHECKED)); |
||||
} |
||||
|
||||
static auto select_device_cb(lv_event_t* ev) { |
||||
Bluetooth* instance = reinterpret_cast<Bluetooth*>(ev->user_data); |
||||
instance->OnDeviceSelected(lv_obj_get_index(ev->target)); |
||||
} |
||||
|
||||
static auto remove_preferred_cb(lv_event_t* ev) { |
||||
Bluetooth* instance = reinterpret_cast<Bluetooth*>(ev->user_data); |
||||
instance->OnDeviceSelected(-1); |
||||
} |
||||
|
||||
Bluetooth::Bluetooth(models::TopBar& bar, |
||||
drivers::Bluetooth& bt, |
||||
drivers::NvsStorage& nvs) |
||||
: MenuScreen(bar, "Bluetooth"), bt_(bt), nvs_(nvs) { |
||||
lv_obj_t* toggle_container = settings_container(content_); |
||||
lv_obj_t* toggle_label = lv_label_create(toggle_container); |
||||
lv_label_set_text(toggle_label, "Enable"); |
||||
lv_obj_set_flex_grow(toggle_label, 1); |
||||
lv_obj_t* toggle = lv_switch_create(toggle_container); |
||||
lv_group_add_obj(group_, toggle); |
||||
|
||||
if (bt.IsEnabled()) { |
||||
lv_obj_add_state(toggle, LV_STATE_CHECKED); |
||||
} |
||||
|
||||
lv_obj_add_event_cb(toggle, toggle_bt_cb, LV_EVENT_VALUE_CHANGED, this); |
||||
|
||||
lv_obj_t* devices_label = lv_label_create(content_); |
||||
lv_label_set_text(devices_label, "Devices"); |
||||
|
||||
devices_list_ = lv_list_create(content_); |
||||
RefreshDevicesList(); |
||||
bt_.SetDeviceDiscovery(true); |
||||
} |
||||
|
||||
Bluetooth::~Bluetooth() { |
||||
bt_.SetDeviceDiscovery(false); |
||||
} |
||||
|
||||
auto Bluetooth::ChangeEnabledState(bool enabled) -> void { |
||||
if (enabled) { |
||||
events::System().RunOnTask([&]() { bt_.Enable(); }); |
||||
nvs_.OutputMode(drivers::NvsStorage::Output::kBluetooth); |
||||
} else { |
||||
events::System().RunOnTask([&]() { bt_.Disable(); }); |
||||
nvs_.OutputMode(drivers::NvsStorage::Output::kHeadphones); |
||||
} |
||||
events::Audio().Dispatch(audio::OutputModeChanged{}); |
||||
RefreshDevicesList(); |
||||
} |
||||
|
||||
auto Bluetooth::RefreshDevicesList() -> void { |
||||
if (!bt_.IsEnabled()) { |
||||
// Bluetooth is disabled, so we just clear the list.
|
||||
RemoveAllDevices(); |
||||
return; |
||||
} |
||||
|
||||
auto devices = bt_.KnownDevices(); |
||||
std::optional<drivers::bluetooth::mac_addr_t> preferred_device = |
||||
nvs_.PreferredBluetoothDevice(); |
||||
|
||||
// If the user's current selection is within the devices list, then we need
|
||||
// to be careful not to rearrange the list items underneath them.
|
||||
lv_obj_t* current_selection = lv_group_get_focused(group_); |
||||
bool is_in_list = current_selection != NULL && |
||||
lv_obj_get_parent(current_selection) == devices_list_; |
||||
|
||||
if (!is_in_list) { |
||||
// The user isn't in the list! We can blow everything away and recreate it
|
||||
// without issues.
|
||||
RemoveAllDevices(); |
||||
|
||||
// First look to see if the user's preferred device is in the list. If it
|
||||
// is, we hoist it up to the top of the list.
|
||||
if (preferred_device) { |
||||
for (size_t i = 0; i < devices.size(); i++) { |
||||
if (devices[i].address == *preferred_device) { |
||||
AddPreferredDevice(devices[i]); |
||||
devices.erase(devices.begin() + i); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// The rest of the list is already sorted by signal strength.
|
||||
for (const auto& device : devices) { |
||||
AddDevice(device); |
||||
} |
||||
} else { |
||||
// The user's selection is within the device list. We need to work out
|
||||
// which devices are new, then add them to the end.
|
||||
for (const auto& mac : macs_in_list_) { |
||||
auto pos = std::find_if( |
||||
devices.begin(), devices.end(), |
||||
[&mac](const auto& device) { return device.address == mac; }); |
||||
|
||||
if (pos != devices.end()) { |
||||
devices.erase(pos); |
||||
} |
||||
} |
||||
|
||||
// The remaining list is now just the new devices.
|
||||
for (const auto& device : devices) { |
||||
if (preferred_device && device.address == *preferred_device) { |
||||
AddPreferredDevice(device); |
||||
} else { |
||||
AddDevice(device); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
auto Bluetooth::RemoveAllDevices() -> void { |
||||
while (lv_obj_get_child_cnt(devices_list_) > 0) { |
||||
lv_obj_del(lv_obj_get_child(devices_list_, 0)); |
||||
} |
||||
macs_in_list_.clear(); |
||||
preferred_device_ = nullptr; |
||||
} |
||||
|
||||
auto Bluetooth::AddPreferredDevice(const drivers::bluetooth::Device& dev) |
||||
-> void { |
||||
preferred_device_ = lv_list_add_btn(devices_list_, NULL, dev.name.c_str()); |
||||
lv_obj_t* remove = lv_btn_create(preferred_device_); |
||||
lv_obj_t* remove_icon = lv_label_create(remove); |
||||
lv_label_set_text(remove_icon, "x"); |
||||
lv_group_add_obj(group_, remove); |
||||
|
||||
macs_in_list_.push_back(dev.address); |
||||
} |
||||
|
||||
auto Bluetooth::AddDevice(const drivers::bluetooth::Device& dev) -> void { |
||||
lv_obj_t* item = lv_list_add_btn(devices_list_, NULL, dev.name.c_str()); |
||||
lv_group_add_obj(group_, item); |
||||
lv_obj_add_event_cb(item, select_device_cb, LV_EVENT_CLICKED, this); |
||||
macs_in_list_.push_back(dev.address); |
||||
} |
||||
|
||||
auto Bluetooth::OnDeviceSelected(ssize_t index) -> void { |
||||
if (index == -1) { |
||||
events::System().RunOnTask([=]() { |
||||
nvs_.PreferredBluetoothDevice({}); |
||||
bt_.SetPreferredDevice({}); |
||||
}); |
||||
RefreshDevicesList(); |
||||
return; |
||||
} |
||||
|
||||
// Tell the bluetooth driver that our preference changed.
|
||||
auto it = macs_in_list_.begin(); |
||||
std::advance(it, index); |
||||
events::System().RunOnTask([=]() { bt_.SetPreferredDevice(*it); }); |
||||
|
||||
// Update which devices are selectable.
|
||||
if (preferred_device_) { |
||||
lv_group_add_obj(group_, preferred_device_); |
||||
// Bubble the newly added object up to its visible position in the list.
|
||||
size_t pos = lv_obj_get_index(preferred_device_); |
||||
while (pos > 0) { |
||||
lv_group_swap_obj(preferred_device_, |
||||
lv_obj_get_child(devices_list_, pos - 1)); |
||||
pos--; |
||||
} |
||||
} |
||||
|
||||
preferred_device_ = lv_obj_get_child(devices_list_, index); |
||||
lv_group_remove_obj(preferred_device_); |
||||
} |
||||
|
||||
static void change_vol_limit_cb(lv_event_t* ev) { |
||||
int selected_index = lv_dropdown_get_selected(ev->target); |
||||
Headphones* instance = reinterpret_cast<Headphones*>(ev->user_data); |
||||
instance->ChangeMaxVolume(selected_index); |
||||
} |
||||
|
||||
static void increase_vol_limit_cb(lv_event_t* ev) { |
||||
Headphones* instance = reinterpret_cast<Headphones*>(ev->user_data); |
||||
instance->ChangeCustomVolume(2); |
||||
} |
||||
|
||||
static void decrease_vol_limit_cb(lv_event_t* ev) { |
||||
Headphones* instance = reinterpret_cast<Headphones*>(ev->user_data); |
||||
instance->ChangeCustomVolume(-2); |
||||
} |
||||
|
||||
Headphones::Headphones(models::TopBar& bar, drivers::NvsStorage& nvs) |
||||
: MenuScreen(bar, "Headphones"), nvs_(nvs), custom_limit_(0) { |
||||
uint16_t reference = drivers::wm8523::kLineLevelReferenceVolume; |
||||
index_to_level_.push_back(reference - (10 * 4)); |
||||
index_to_level_.push_back(reference + (6 * 4)); |
||||
index_to_level_.push_back(reference + (9.5 * 4)); |
||||
|
||||
lv_obj_t* vol_label = lv_label_create(content_); |
||||
lv_label_set_text(vol_label, "Volume Limit"); |
||||
lv_obj_t* vol_dropdown = lv_dropdown_create(content_); |
||||
lv_obj_set_width(vol_dropdown, lv_pct(100)); |
||||
lv_dropdown_set_options( |
||||
vol_dropdown, |
||||
"Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)\nCustom"); |
||||
lv_group_add_obj(group_, vol_dropdown); |
||||
themes::Theme::instance()->ApplyStyle(lv_dropdown_get_list(vol_dropdown), |
||||
themes::Style::kPopup); |
||||
|
||||
uint16_t level = nvs.AmpMaxVolume(); |
||||
for (int i = 0; i < index_to_level_.size() + 1; i++) { |
||||
if (i == index_to_level_.size() || index_to_level_[i] == level) { |
||||
lv_dropdown_set_selected(vol_dropdown, i); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
lv_obj_add_event_cb(vol_dropdown, change_vol_limit_cb, LV_EVENT_VALUE_CHANGED, |
||||
this); |
||||
|
||||
custom_vol_container_ = settings_container(content_); |
||||
|
||||
lv_obj_t* decrease_btn = lv_btn_create(custom_vol_container_); |
||||
lv_obj_t* btn_label = lv_label_create(decrease_btn); |
||||
lv_label_set_text(btn_label, "-"); |
||||
lv_obj_add_event_cb(decrease_btn, decrease_vol_limit_cb, LV_EVENT_CLICKED, |
||||
this); |
||||
|
||||
custom_vol_label_ = lv_label_create(custom_vol_container_); |
||||
UpdateCustomVol(level); |
||||
|
||||
lv_obj_t* increase_btn = lv_btn_create(custom_vol_container_); |
||||
btn_label = lv_label_create(increase_btn); |
||||
lv_label_set_text(btn_label, "+"); |
||||
lv_obj_add_event_cb(increase_btn, increase_vol_limit_cb, LV_EVENT_CLICKED, |
||||
this); |
||||
|
||||
if (lv_dropdown_get_selected(vol_dropdown) != index_to_level_.size()) { |
||||
lv_obj_add_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN); |
||||
} |
||||
|
||||
lv_obj_t* spacer = lv_obj_create(content_); |
||||
lv_obj_set_size(spacer, 1, 4); |
||||
|
||||
lv_obj_t* balance_label = lv_label_create(content_); |
||||
lv_label_set_text(balance_label, "Left/Right Balance"); |
||||
|
||||
spacer = lv_obj_create(content_); |
||||
lv_obj_set_size(spacer, 1, 4); |
||||
|
||||
lv_obj_t* balance = lv_slider_create(content_); |
||||
lv_obj_set_size(balance, lv_pct(100), 5); |
||||
lv_slider_set_range(balance, -10, 10); |
||||
lv_slider_set_value(balance, 0, LV_ANIM_OFF); |
||||
lv_slider_set_mode(balance, LV_SLIDER_MODE_SYMMETRICAL); |
||||
lv_group_add_obj(group_, balance); |
||||
lv_obj_t* current_balance_label = lv_label_create(content_); |
||||
lv_label_set_text(current_balance_label, "0dB"); |
||||
lv_obj_set_size(current_balance_label, lv_pct(100), LV_SIZE_CONTENT); |
||||
|
||||
lv_obj_move_foreground(lv_dropdown_get_list(vol_dropdown)); |
||||
} |
||||
|
||||
auto Headphones::ChangeMaxVolume(uint8_t index) -> void { |
||||
if (index >= index_to_level_.size()) { |
||||
lv_obj_clear_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN); |
||||
return; |
||||
} |
||||
auto vol = index_to_level_[index]; |
||||
lv_obj_add_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN); |
||||
UpdateCustomVol(vol); |
||||
events::Audio().Dispatch(audio::ChangeMaxVolume{.new_max = vol}); |
||||
} |
||||
|
||||
auto Headphones::ChangeCustomVolume(int8_t diff) -> void { |
||||
UpdateCustomVol(custom_limit_ + diff); |
||||
} |
||||
|
||||
auto Headphones::UpdateCustomVol(uint16_t level) -> void { |
||||
custom_limit_ = level; |
||||
int16_t db = (static_cast<int32_t>(level) - |
||||
drivers::wm8523::kLineLevelReferenceVolume) / |
||||
4; |
||||
int16_t db_parts = (static_cast<int32_t>(level) - |
||||
drivers::wm8523::kLineLevelReferenceVolume) % |
||||
4; |
||||
|
||||
std::ostringstream builder; |
||||
if (db >= 0) { |
||||
builder << "+"; |
||||
} |
||||
builder << db << "."; |
||||
builder << (db_parts * 100 / 4); |
||||
builder << " dBV"; |
||||
|
||||
lv_label_set_text(custom_vol_label_, builder.str().c_str()); |
||||
} |
||||
|
||||
static void change_brightness_cb(lv_event_t* ev) { |
||||
Appearance* instance = reinterpret_cast<Appearance*>(ev->user_data); |
||||
instance->ChangeBrightness(lv_slider_get_value(ev->target)); |
||||
} |
||||
|
||||
static void release_brightness_cb(lv_event_t* ev) { |
||||
Appearance* instance = reinterpret_cast<Appearance*>(ev->user_data); |
||||
instance->CommitBrightness(); |
||||
} |
||||
|
||||
static auto brightness_str(uint_fast8_t percent) -> std::string { |
||||
return std::to_string(percent) + "%"; |
||||
} |
||||
|
||||
Appearance::Appearance(models::TopBar& bar, |
||||
drivers::NvsStorage& nvs, |
||||
drivers::Display& display) |
||||
: MenuScreen(bar, "Appearance"), nvs_(nvs), display_(display) { |
||||
lv_obj_t* toggle_container = settings_container(content_); |
||||
lv_obj_t* toggle_label = lv_label_create(toggle_container); |
||||
lv_obj_set_flex_grow(toggle_label, 1); |
||||
lv_label_set_text(toggle_label, "Dark Mode"); |
||||
lv_obj_t* toggle = lv_switch_create(toggle_container); |
||||
lv_group_add_obj(group_, toggle); |
||||
|
||||
uint_fast8_t initial_brightness = nvs_.ScreenBrightness(); |
||||
|
||||
lv_obj_t* brightness_label = lv_label_create(content_); |
||||
lv_label_set_text(brightness_label, "Brightness"); |
||||
lv_obj_t* brightness = lv_slider_create(content_); |
||||
lv_obj_set_size(brightness, lv_pct(100), 5); |
||||
lv_slider_set_range(brightness, 10, 100); |
||||
lv_slider_set_value(brightness, initial_brightness, LV_ANIM_OFF); |
||||
lv_group_add_obj(group_, brightness); |
||||
current_brightness_label_ = lv_label_create(content_); |
||||
lv_label_set_text(current_brightness_label_, |
||||
brightness_str(initial_brightness).c_str()); |
||||
lv_obj_set_size(current_brightness_label_, lv_pct(100), LV_SIZE_CONTENT); |
||||
|
||||
lv_obj_add_event_cb(brightness, change_brightness_cb, LV_EVENT_VALUE_CHANGED, |
||||
this); |
||||
lv_obj_add_event_cb(brightness, release_brightness_cb, LV_EVENT_RELEASED, |
||||
this); |
||||
} |
||||
|
||||
auto Appearance::ChangeBrightness(uint_fast8_t new_level) -> void { |
||||
current_brightness_ = new_level; |
||||
display_.SetBrightness(new_level); |
||||
lv_label_set_text(current_brightness_label_, |
||||
brightness_str(new_level).c_str()); |
||||
} |
||||
|
||||
auto Appearance::CommitBrightness() -> void { |
||||
nvs_.ScreenBrightness(current_brightness_); |
||||
} |
||||
|
||||
InputMethod::InputMethod(models::TopBar& bar, drivers::NvsStorage& nvs) |
||||
: MenuScreen(bar, "Input Method"), nvs_(nvs) { |
||||
lv_obj_t* primary_label = lv_label_create(content_); |
||||
lv_label_set_text(primary_label, "Control scheme"); |
||||
lv_obj_t* primary_dropdown = lv_dropdown_create(content_); |
||||
lv_dropdown_set_options( |
||||
primary_dropdown, |
||||
"Side buttons only\nButtons and touch\nD-Pad\nClickwheel"); |
||||
lv_group_add_obj(group_, primary_dropdown); |
||||
|
||||
lv_dropdown_set_selected(primary_dropdown, |
||||
static_cast<uint16_t>(nvs.PrimaryInput())); |
||||
themes::Theme::instance()->ApplyStyle(lv_dropdown_get_list(primary_dropdown), |
||||
themes::Style::kPopup); |
||||
|
||||
lv_bind(primary_dropdown, LV_EVENT_VALUE_CHANGED, [this](lv_obj_t* obj) { |
||||
drivers::NvsStorage::InputModes mode; |
||||
switch (lv_dropdown_get_selected(obj)) { |
||||
case 0: |
||||
mode = drivers::NvsStorage::InputModes::kButtonsOnly; |
||||
break; |
||||
case 1: |
||||
mode = drivers::NvsStorage::InputModes::kButtonsWithWheel; |
||||
break; |
||||
case 2: |
||||
mode = drivers::NvsStorage::InputModes::kDirectionalWheel; |
||||
break; |
||||
case 3: |
||||
mode = drivers::NvsStorage::InputModes::kRotatingWheel; |
||||
break; |
||||
default: |
||||
return; |
||||
} |
||||
nvs_.PrimaryInput(mode); |
||||
events::Ui().Dispatch(internal::ControlSchemeChanged{}); |
||||
}); |
||||
} |
||||
|
||||
Storage::Storage(models::TopBar& bar) : MenuScreen(bar, "Storage") { |
||||
label_pair(content_, "Storage Capacity:", "32 GiB"); |
||||
label_pair(content_, "Currently Used:", "6 GiB"); |
||||
label_pair(content_, "DB Size:", "1.2 MiB"); |
||||
|
||||
lv_obj_t* usage_bar = lv_bar_create(content_); |
||||
lv_bar_set_range(usage_bar, 0, 32); |
||||
lv_bar_set_value(usage_bar, 6, LV_ANIM_OFF); |
||||
|
||||
lv_obj_t* container = lv_obj_create(content_); |
||||
lv_obj_set_size(container, lv_pct(100), 30); |
||||
lv_obj_set_layout(container, LV_LAYOUT_FLEX); |
||||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW); |
||||
lv_obj_set_flex_align(container, LV_FLEX_ALIGN_SPACE_EVENLY, |
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); |
||||
|
||||
lv_obj_t* reset_btn = lv_btn_create(container); |
||||
lv_obj_t* reset_label = lv_label_create(reset_btn); |
||||
lv_label_set_text(reset_label, "Update Database"); |
||||
lv_group_add_obj(group_, reset_btn); |
||||
themes::Theme::instance()->ApplyStyle(reset_btn, |
||||
themes::Style::kButtonPrimary); |
||||
|
||||
lv_bind(reset_btn, LV_EVENT_CLICKED, [&](lv_obj_t*) { |
||||
events::Ui().Dispatch(internal::ReindexDatabase{}); |
||||
}); |
||||
} |
||||
|
||||
FirmwareUpdate::FirmwareUpdate(models::TopBar& bar, drivers::Samd& samd) |
||||
: MenuScreen(bar, "Firmware Update") { |
||||
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, |
||||
LV_FLEX_ALIGN_CENTER); |
||||
|
||||
auto samd_ver = samd.Version(); |
||||
label_pair(content_, "SAMD21 FW:", {samd_ver.data(), samd_ver.size()}); |
||||
|
||||
lv_obj_t* spacer = lv_obj_create(content_); |
||||
lv_obj_set_size(spacer, 1, 4); |
||||
|
||||
lv_obj_t* flash_esp_btn = lv_btn_create(content_); |
||||
lv_obj_t* flash_esp_label = lv_label_create(flash_esp_btn); |
||||
lv_label_set_text(flash_esp_label, "Update"); |
||||
lv_group_add_obj(group_, flash_esp_btn); |
||||
themes::Theme::instance()->ApplyStyle(flash_esp_btn, |
||||
themes::Style::kButtonPrimary); |
||||
|
||||
spacer = lv_obj_create(content_); |
||||
lv_obj_set_size(spacer, 1, 8); |
||||
|
||||
auto desc = esp_app_get_description(); |
||||
label_pair(content_, "ESP32 FW:", desc->version); |
||||
|
||||
spacer = lv_obj_create(content_); |
||||
lv_obj_set_size(spacer, 1, 4); |
||||
|
||||
lv_obj_t* flash_samd_btn = lv_btn_create(content_); |
||||
lv_obj_t* flash_samd_label = lv_label_create(flash_samd_btn); |
||||
lv_label_set_text(flash_samd_label, "Update"); |
||||
lv_group_add_obj(group_, flash_samd_btn); |
||||
themes::Theme::instance()->ApplyStyle(flash_samd_btn, |
||||
themes::Style::kButtonPrimary); |
||||
} |
||||
|
||||
About::About(models::TopBar& bar) : MenuScreen(bar, "About") { |
||||
lv_obj_t* label = lv_label_create(content_); |
||||
lv_label_set_text(label, "Some licenses or whatever"); |
||||
} |
||||
|
||||
} // namespace screens
|
||||
} // namespace ui
|
@ -1,61 +0,0 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#include "widget_top_bar.hpp" |
||||
#include "battery.hpp" |
||||
#include "core/lv_group.h" |
||||
#include "core/lv_obj.h" |
||||
#include "event_queue.hpp" |
||||
#include "extra/layouts/flex/lv_flex.h" |
||||
#include "font/lv_symbol_def.h" |
||||
#include "model_top_bar.hpp" |
||||
#include "themes.hpp" |
||||
#include "track.hpp" |
||||
#include "ui_events.hpp" |
||||
#include "ui_fsm.hpp" |
||||
#include "widgets/lv_img.h" |
||||
#include "widgets/lv_label.h" |
||||
|
||||
namespace ui { |
||||
namespace widgets { |
||||
|
||||
static void back_click_cb(lv_event_t* ev) { |
||||
events::Ui().Dispatch(internal::BackPressed{}); |
||||
} |
||||
|
||||
TopBar::TopBar(lv_obj_t* parent, |
||||
const Configuration& config, |
||||
models::TopBar& model) { |
||||
container_ = lv_obj_create(parent); |
||||
lv_obj_set_size(container_, lv_pct(100), 20); |
||||
lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_ROW); |
||||
lv_obj_set_flex_align(container_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, |
||||
LV_FLEX_ALIGN_END); |
||||
lv_obj_set_style_pad_column(container_, 5, LV_PART_MAIN); |
||||
|
||||
if (config.show_back_button) { |
||||
back_button_ = lv_btn_create(container_); |
||||
lv_obj_set_size(back_button_, LV_SIZE_CONTENT, 12); |
||||
lv_obj_t* button_icon = lv_label_create(back_button_); |
||||
lv_label_set_text(button_icon, "<"); |
||||
lv_obj_add_event_cb(back_button_, back_click_cb, LV_EVENT_CLICKED, NULL); |
||||
lv_obj_center(button_icon); |
||||
} else { |
||||
back_button_ = nullptr; |
||||
} |
||||
|
||||
lv_obj_t* title_ = lv_label_create(container_); |
||||
lv_obj_set_height(title_, 17); |
||||
lv_obj_set_flex_grow(title_, 1); |
||||
|
||||
lv_label_set_text(title_, config.title.c_str()); |
||||
lv_label_set_long_mode(title_, LV_LABEL_LONG_DOT); |
||||
|
||||
themes::Theme::instance()->ApplyStyle(container_, themes::Style::kTopBar); |
||||
} |
||||
|
||||
} // namespace widgets
|
||||
} // namespace ui
|
Loading…
Reference in new issue