parent
f168bfab76
commit
f09ba5ffd5
@ -0,0 +1,72 @@ |
|||||||
|
AccessModifierOffset: -4 |
||||||
|
AlignAfterOpenBracket: Align |
||||||
|
AlignConsecutiveAssignments: true |
||||||
|
AlignConsecutiveDeclarations: true |
||||||
|
AlignEscapedNewlinesLeft: true |
||||||
|
AlignOperands: true |
||||||
|
AlignTrailingComments: true |
||||||
|
AllowAllParametersOfDeclarationOnNextLine: true |
||||||
|
AllowShortBlocksOnASingleLine: false |
||||||
|
AllowShortFunctionsOnASingleLine: None |
||||||
|
AllowShortIfStatementsOnASingleLine: false |
||||||
|
AllowShortLoopsOnASingleLine: false |
||||||
|
AlwaysBreakBeforeMultilineStrings: false |
||||||
|
AlwaysBreakTemplateDeclarations: true |
||||||
|
BinPackArguments: false |
||||||
|
BinPackParameters: false |
||||||
|
BraceWrapping: |
||||||
|
AfterClass: true |
||||||
|
AfterControlStatement: true |
||||||
|
AfterEnum: true |
||||||
|
AfterFunction: true |
||||||
|
AfterNamespace: true |
||||||
|
AfterObjCDeclaration: true |
||||||
|
AfterStruct: true |
||||||
|
AfterUnion: true |
||||||
|
BeforeCatch: true |
||||||
|
BeforeElse: true |
||||||
|
IndentBraces: false |
||||||
|
BreakBeforeBinaryOperators: NonAssignment |
||||||
|
BreakBeforeBraces: Custom |
||||||
|
BreakBeforeTernaryOperators: true |
||||||
|
BreakConstructorInitializersBeforeComma: true |
||||||
|
ColumnLimit: 120 |
||||||
|
CommentPragmas: '^!' |
||||||
|
ConstructorInitializerAllOnOneLineOrOnePerLine: false |
||||||
|
ConstructorInitializerIndentWidth: 0 |
||||||
|
ContinuationIndentWidth: 4 |
||||||
|
Cpp11BracedListStyle: true |
||||||
|
DerivePointerAlignment: false |
||||||
|
DisableFormat: false |
||||||
|
ExperimentalAutoDetectBinPacking: false |
||||||
|
ForEachMacros: [ foreach, BOOST_FOREACH ] |
||||||
|
IndentCaseLabels: true |
||||||
|
IndentFunctionDeclarationAfterType: false |
||||||
|
IndentWidth: 4 |
||||||
|
IndentWrappedFunctionNames: false |
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: true |
||||||
|
Language: Cpp |
||||||
|
MaxEmptyLinesToKeep: 2 |
||||||
|
NamespaceIndentation: None |
||||||
|
ObjCSpaceAfterProperty: false |
||||||
|
ObjCSpaceBeforeProtocolList: true |
||||||
|
PenaltyBreakBeforeFirstCallParameter: 19 |
||||||
|
PenaltyBreakComment: 300 |
||||||
|
PenaltyBreakFirstLessLess: 120 |
||||||
|
PenaltyBreakString: 1000 |
||||||
|
PenaltyExcessCharacter: 100 |
||||||
|
PenaltyReturnTypeOnItsOwnLine: 600 |
||||||
|
PointerAlignment: Left |
||||||
|
SpaceAfterCStyleCast: true |
||||||
|
SpaceBeforeAssignmentOperators: true |
||||||
|
SpaceBeforeParens: ControlStatements |
||||||
|
SpaceInEmptyParentheses: false |
||||||
|
SpacesBeforeTrailingComments: 1 |
||||||
|
SpacesInAngles: false |
||||||
|
SpacesInContainerLiterals: true |
||||||
|
SpacesInCStyleCastParentheses: false |
||||||
|
SpacesInParentheses: true |
||||||
|
SpacesInSquareBrackets: false |
||||||
|
Standard: Cpp11 |
||||||
|
TabWidth: 4 |
||||||
|
UseTab: Never |
@ -0,0 +1,44 @@ |
|||||||
|
name: ci |
||||||
|
|
||||||
|
on: [pull_request] |
||||||
|
|
||||||
|
env: |
||||||
|
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) |
||||||
|
BUILD_TYPE: Release |
||||||
|
|
||||||
|
jobs: |
||||||
|
Build-And-Test: |
||||||
|
runs-on: macos-latest |
||||||
|
|
||||||
|
steps: |
||||||
|
- uses: actions/checkout@v2 |
||||||
|
with: |
||||||
|
submodules: true |
||||||
|
|
||||||
|
- name: Create Build Environment |
||||||
|
# Some projects don't allow in-source building, so create a separate build directory |
||||||
|
# We'll use this as our working directory for all subsequent commands |
||||||
|
run: cmake -E make_directory ${{runner.workspace}}/build |
||||||
|
|
||||||
|
- name: Configure |
||||||
|
shell: bash |
||||||
|
working-directory: ${{runner.workspace}}/build |
||||||
|
run: cmake $GITHUB_WORKSPACE -GXcode -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBINDEY_BUILD_TESTS=ON |
||||||
|
env: |
||||||
|
CC: clang |
||||||
|
CXX: clang |
||||||
|
|
||||||
|
- name: Build |
||||||
|
working-directory: ${{runner.workspace}}/build |
||||||
|
shell: bash |
||||||
|
run: cmake --build . --config $BUILD_TYPE |
||||||
|
env: |
||||||
|
CC: clang |
||||||
|
CXX: clang |
||||||
|
|
||||||
|
- name: Test |
||||||
|
working-directory: ${{runner.workspace}}/build |
||||||
|
shell: bash |
||||||
|
# Execute tests defined by the CMake configuration. |
||||||
|
# See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail |
||||||
|
run: ctest -C $BUILD_TYPE |
@ -0,0 +1,5 @@ |
|||||||
|
*.a |
||||||
|
*.lib |
||||||
|
*.o |
||||||
|
*.pdb |
||||||
|
.DS_Store |
@ -0,0 +1,6 @@ |
|||||||
|
[submodule "lib/Catch2"] |
||||||
|
path = lib/Catch2 |
||||||
|
url = git@github.com:catchorg/Catch2.git |
||||||
|
[submodule "lib/nod"] |
||||||
|
path = lib/nod |
||||||
|
url = git@github.com:fr00b0/nod.git |
@ -0,0 +1,6 @@ |
|||||||
|
# Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||||
|
# |
||||||
|
# SPDX-License-Identifier: GPL-3.0-only |
||||||
|
idf_component_register( |
||||||
|
INCLUDE_DIRS "include" |
||||||
|
) |
@ -0,0 +1,21 @@ |
|||||||
|
## The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2021 Kevin Dixon |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,116 @@ |
|||||||
|
# bindey |
||||||
|
|
||||||
|
Everyone knows Model-View-ViewModel is the best architecture, but how can we realize it in C++ applications with minimal overhead, and no complicated framework impositions? |
||||||
|
|
||||||
|
`bindey` provides the basic building block of MVVM -- an observable "Property" and a databinding mechanism. |
||||||
|
|
||||||
|
## Property Usage |
||||||
|
|
||||||
|
At minimum, `bindey::property` can allow you to avoid writing getters and setters. Consider this example: |
||||||
|
|
||||||
|
``` |
||||||
|
#include <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; |
||||||
|
}; |
||||||
|
``` |
@ -0,0 +1,47 @@ |
|||||||
|
#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
|
@ -0,0 +1,137 @@ |
|||||||
|
#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
|
@ -0,0 +1,681 @@ |
|||||||
|
#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,23 @@ |
|||||||
|
/*
|
||||||
|
* 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
|
@ -0,0 +1,30 @@ |
|||||||
|
/*
|
||||||
|
* 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
|
@ -0,0 +1,26 @@ |
|||||||
|
/*
|
||||||
|
* 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
|
Loading…
Reference in new issue