Skip to content

Instantly share code, notes, and snippets.

Created February 7, 2016 02:55
Show Gist options
  • Save anonymous/c8f633788ddaeb24c291 to your computer and use it in GitHub Desktop.
Save anonymous/c8f633788ddaeb24c291 to your computer and use it in GitHub Desktop.
Ugly code by Prismatic
/*
Copyright (C) 2016 Preet Desai (preet.desai@gmail.com)
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.
*/
#include <raintk/test/RainTkTestContext.hpp>
#include <raintk/RainTkListModelSTLVector.hpp>
#include <raintk/RainTkListDelegate.hpp>
#include <raintk/RainTkScrollArea.hpp>
#include <raintk/RainTkRectangle.hpp>
// =========================================================== //
// =========================================================== //
namespace raintk
{
struct TestItem
{
glm::u8vec3 color;
};
class TestDelegate : public ListDelegate
{
public:
TestDelegate(ks::Object::Key const &key,
shared_ptr<Widget> parent) :
ListDelegate(key,parent),
m_index(0)
{}
void Init(ks::Object::Key const &,
shared_ptr<TestDelegate> const &this_delegate)
{
width = 5*mm;
height = 10*mm; // make height random
m_rect = ks::make_object<Rectangle>(this_delegate,"");
m_rect->width = [this](){ return width.Get(); };
m_rect->height = [this](){ return height.Get(); };
}
~TestDelegate()
{
}
void SetIndex(uint index)
{
m_index = index;
width = (5+m_index)*mm;
}
uint GetIndex() const
{
return m_index;
}
void SetData(TestItem const &item)
{
m_color = item.color;
m_rect->color = m_color;
}
private:
uint m_index;
shared_ptr<Rectangle> m_rect;
glm::vec3 m_color;
};
// =========================================================== //
class ListViewDelegateHeightInvalid : public ks::Exception
{
public:
ListViewDelegateHeightInvalid(std::string msg) :
ks::Exception(ks::Exception::ErrorLevel::ERROR,msg)
{}
~ListViewDelegateHeightInvalid() = default;
};
class ListViewTODO : public ks::Exception
{
public:
ListViewTODO() :
ks::Exception(ks::Exception::ErrorLevel::ERROR,"")
{}
~ListViewTODO() = default;
};
// =========================================================== //
struct ListViewProperties
{
enum class Layout
{
Row,
Column
};
enum class Order
{
Ascending,
Descending
};
};
// =========================================================== //
// =========================================================== //
// =========================================================== //
template<typename ItemType, typename DelegateType>
class ListView : public raintk::ScrollArea
{
using ListViewType = ListView<ItemType,DelegateType>;
using DelegateList =
std::vector<shared_ptr<DelegateType>>;
using DelegateRange =
std::pair<
typename DelegateList::iterator,
typename DelegateList::iterator
>;
shared_ptr<ListModel<ItemType>> m_list_model;
// * Connections
Id m_cid_content_x_changed;
Id m_cid_content_y_changed;
Id m_cid_before_adding_items;
Id m_cid_added_items;
Id m_cid_before_removing_items;
Id m_cid_removed_items;
Id m_cid_data_changed;
Id m_cid_layout_changed;
// * Delegate bounds
DynamicPropertyPullNotify<float> m_delegate_bounds_top{
name+".delegate_bounds_top",0.0f
};
DynamicPropertyPullNotify<float> m_delegate_bounds_bottom{
name+".delegate_bounds_bottom",0.0f
};
DynamicPropertyPullNotify<float> m_delegate_bounds_left{
name+".delegate_bounds_left",0.0f
};
DynamicPropertyPullNotify<float> m_delegate_bounds_right{
name+".delegate_bounds_right",0.0f
};
// These properties point to the four above properties
// depending on the layout direction and order
DynamicPropertyPullNotify<float>* m_delegate_bounds_start;
DynamicPropertyPullNotify<float>* m_delegate_bounds_end;
DynamicPropertyPullNotify<float>* m_view_size;
DynamicPropertyPullNotify<float>* m_content_size;
DynamicPropertyPullNotify<float>* m_content_position;
// * The average height or width of a delegate, used
// to estimate the dimensions of the content_parent
// and place the first delegate
float m_average_delegate_size{0};
float m_minimum_delegate_size{1.0f*mm}; // TODO setter/getter
uint m_max_delegate_count{30}; // TODO setter/getter
std::function<float(shared_ptr<DelegateType> const &)> m_get_delegate_size;
std::function<float(shared_ptr<DelegateType> const &)> m_get_delegate_position;
std::function<void(shared_ptr<DelegateType> const &,float)> m_set_delegate_position;
DelegateList m_list_delegates;
// * Guidelines to help debug, should be disabled
// in release builds
using ListRectangles = std::vector<shared_ptr<Rectangle>>;
ListRectangles m_list_guidelines;
public:
// Properties
DynamicPropertyPullNotify<float> delegate_extents{
name+"delegate_extents",25*mm
};
DynamicPropertyPullNotify<float> spacing{
name+".spacing",0.0f
};
DynamicPropertyPullNotify<ListViewProperties::Layout> layout{
name+".layout",ListViewProperties::Layout::Column
};
// === //
using base_type = raintk::ScrollArea;
ListView(ks::Object::Key const &key,
shared_ptr<Widget> parent,
std::string name) :
raintk::ScrollArea(key,parent,std::move(name))
{}
~ListView()
{}
void Init(ks::Object::Key const &,
shared_ptr<ListViewType> const &this_view)
{
setupLayoutDirection();
// Setup connections
// Setup a slot for both content x and y changed
// signals so we don't have to change them when
// the layout direction changes -- the ScrollArea
// should lock respective directions and prevent
// unused signals anyway
m_cid_content_y_changed =
m_content_parent->y.signal_changed.Connect(
this_view,
&ListViewType::onScrollPositionChanged,
ks::ConnectionType::Direct);
m_cid_content_x_changed =
m_content_parent->x.signal_changed.Connect(
this_view,
&ListViewType::onScrollPositionChanged,
ks::ConnectionType::Direct);
layout.signal_changed.Connect(
this_view,
&ListViewType::onLayoutDirectionChanged,
ks::ConnectionType::Direct);
}
void SetListModel(shared_ptr<ListModel<ItemType>> list_model)
{
// Remove all previous delegates
for(auto& delegate : m_list_delegates)
{
this->RemoveChild(delegate);
}
m_list_delegates.clear();
// Disconnect previous connections
if(m_list_model)
{
m_list_model->signal_before_adding_items.Disconnect(
m_cid_before_adding_items);
m_list_model->signal_added_items.Disconnect(
m_cid_added_items);
m_list_model->signal_before_removing_items.Disconnect(
m_cid_before_removing_items);
m_list_model->signal_removed_items.Disconnect(
m_cid_removed_items);
m_list_model->signal_data_changed.Disconnect(
m_cid_data_changed);
m_list_model->signal_layout_changed.Disconnect(
m_cid_layout_changed);
}
// Setup new connections
m_list_model = list_model;
shared_ptr<ListViewType> this_view =
std::static_pointer_cast<ListViewType>(
shared_from_this());
m_cid_before_adding_items =
m_list_model->signal_before_adding_items.Connect(
this_view,
&ListViewType::onBeforeAddingItems,
ks::ConnectionType::Direct);
m_cid_added_items = m_list_model->signal_added_items.Connect(
this_view,
&ListViewType::onAddedItems,
ks::ConnectionType::Direct);
m_cid_before_removing_items =
m_list_model->signal_before_removing_items.Connect(
this_view,
&ListViewType::onBeforeRemovingItems,
ks::ConnectionType::Direct);
m_cid_removed_items =
m_list_model->signal_removed_items.Connect(
this_view,
&ListViewType::onRemovedItems,
ks::ConnectionType::Direct);
m_cid_data_changed =
m_list_model->signal_data_changed.Connect(
this_view,
&ListViewType::onDataChanged,
ks::ConnectionType::Direct);
m_cid_layout_changed =
m_list_model->signal_layout_changed.Connect(
this_view,
&ListViewType::onLayoutChanged,
ks::ConnectionType::Direct);
m_content_parent->x = 0;
m_content_parent->y = 0;
m_content_parent->width = width.Get();
m_content_parent->height = height.Get();
m_cmlist_update_data->GetComponent(m_entity_id).
update |= UpdateData::UpdateWidget;
}
private:
void onBeforeAddingItems(uint idx_before,
uint count)
{
(void)idx_before;
(void)count;
}
void onAddedItems(uint idx_first_added,
uint idx_end_added)
{
(void)idx_first_added;
(void)idx_end_added;
}
void onBeforeRemovingItems(uint idx_first_remove,
uint idx_end_remove)
{
(void)idx_first_remove;
(void)idx_end_remove;
}
void onRemovedItems(uint idx_after_removed,
uint count)
{
(void)idx_after_removed;
(void)count;
}
void onDelegateDimChanged()
{
// Mark this widget as updated
m_cmlist_update_data->GetComponent(m_entity_id).
update |= UpdateData::UpdateWidget;
}
void onDataChanged(uint)
{
}
void onLayoutChanged()
{
}
void setupLayoutDirection()
{
if(layout.Get()==ListViewProperties::Layout::Column)
{
m_view_size = &(height);
m_content_size = &(m_content_parent->height);
m_content_position = &(m_content_parent->y);
m_delegate_bounds_start = &(m_delegate_bounds_top);
m_delegate_bounds_end = &(m_delegate_bounds_bottom);
m_get_delegate_size =
[](shared_ptr<DelegateType> const &d) -> float {
return d->height.Get();
};
m_get_delegate_position =
[](shared_ptr<DelegateType> const &d) -> float {
return d->y.Get();
};
m_set_delegate_position =
[](shared_ptr<DelegateType> const &d, float new_position) {
return d->y = new_position;
};
}
else
{
m_view_size = &(width);
m_content_size = &(m_content_parent->width);
m_content_position = &(m_content_parent->x);
m_delegate_bounds_start = &(m_delegate_bounds_left);
m_delegate_bounds_end = &(m_delegate_bounds_right);
m_get_delegate_size =
[](shared_ptr<DelegateType> const &d) -> float {
return d->width.Get();
};
m_get_delegate_position =
[](shared_ptr<DelegateType> const &d) -> float {
return d->x.Get();
};
m_set_delegate_position =
[](shared_ptr<DelegateType> const &d, float new_position) {
return d->x = new_position;
};
}
calcDelegateBounds();
createDebugGuidelines();
}
void onLayoutDirectionChanged()
{
setupLayoutDirection();
}
void onScrollPositionChanged()
{
m_cmlist_update_data->GetComponent(m_entity_id).
update |= UpdateData::UpdateWidget;
}
void update()
{
if(m_list_model==nullptr || m_list_model->GetSize()==0)
{
return;
}
// Erase all delegates outside of the delegate extents
eraseDelegatesOutsideExtents(
m_delegate_bounds_top.Get(),
m_delegate_bounds_bottom.Get(),
m_delegate_bounds_left.Get(),
m_delegate_bounds_right.Get());
if(m_list_delegates.empty())
{
// Create the first delegate using an estimated
// index based on the current position of the list
calcAverageDelegateSize();
updateContentParentSize();
// Clamp the current position to be within the
// content size
clampContentPositionToSize();
// Get model index based on content position
float estimated_model_index =
(m_content_position->Get()*-1.0f)/
(m_average_delegate_size+spacing.Get());
// Create delegate for model index
m_list_delegates.push_back(
ks::make_object<DelegateType>(
m_content_parent));
m_list_delegates.back()->SetData(
m_list_model->GetData(
estimated_model_index));
m_list_delegates.back()->SetIndex(
estimated_model_index);
m_set_delegate_position(
m_list_delegates.back(),
m_content_position->Get());
}
// There must be at least one delegate at this point
fillSpaceBefore();
fillSpaceAfter();
correctDelegatePositions();
calcAverageDelegateSize();
updateContentParentSize();
}
// Fill available space before the first delegate
// in list_delegates
void fillSpaceBefore()
{
sint model_index = m_list_delegates.front()->GetIndex();
model_index--;
// ie. The edge closest towards the start
// of the delegate bounds
float first_start_position =
m_get_delegate_position(
m_list_delegates.front());
float space_before =
first_start_position-m_delegate_bounds_start->Get();
while((space_before > 0) &&
(model_index >= 0) &&
(m_list_delegates.size() <= m_max_delegate_count))
{
auto delegate =
ks::make_object<DelegateType>(
m_content_parent);
delegate->SetData(m_list_model->GetData(model_index));
delegate->SetIndex(model_index);
m_list_delegates.insert(
m_list_delegates.begin(),
delegate);
first_start_position -= (spacing.Get()+m_get_delegate_size(delegate));
m_set_delegate_position(delegate,first_start_position);
space_before = first_start_position-m_delegate_bounds_start->Get();
model_index--;
}
}
// Fill available space after the last delegate
// in list_delegates
void fillSpaceAfter()
{
uint model_index = m_list_delegates.back()->GetIndex();
model_index++;
// ie. The edge closest towards the end of
// the delegate bounds
float last_end_position =
m_get_delegate_position(
m_list_delegates.back())+
m_get_delegate_size(
m_list_delegates.back());
float space_after =
m_delegate_bounds_end->Get()-last_end_position;
while((space_after > 0) &&
(model_index < m_list_model->GetSize()) &&
(m_list_delegates.size() <= m_max_delegate_count))
{
auto delegate =
ks::make_object<DelegateType>(
m_content_parent);
delegate->SetData(m_list_model->GetData(model_index));
delegate->SetIndex(model_index);
m_list_delegates.push_back(delegate);
last_end_position += spacing.Get();
m_set_delegate_position(delegate,last_end_position);
last_end_position += m_get_delegate_size(delegate);
space_after = m_delegate_bounds_end->Get()-last_end_position;
model_index++;
}
}
void correctDelegatePositions()
{
// If the first delegate isn't currently instantiated,
// we don't need to correct positions
if(m_list_delegates.front()->GetIndex() != 0 ||
m_get_delegate_position(m_list_delegates.front())==0.0f)
{
return;
}
// Shift all the delegates along to the correct position
float position_shift =
m_get_delegate_position(
m_list_delegates.front())*-1.0f;
for(auto& delegate : m_list_delegates)
{
float new_position =
m_get_delegate_position(delegate)+
position_shift;
m_set_delegate_position(
delegate,new_position);
}
// Shift the current position of the content_parent so
// that the view doesn't 'jump'
m_content_position->Assign(
std::max<float>(
m_content_position->Get()+position_shift,
0.0f));
}
void clampContentPositionToSize()
{
if(m_content_size->Get() > m_view_size->Get())
{
if(m_content_position->Get() > 0)
{
m_content_position->Assign(0);
}
else if(m_content_position->Get() <
(m_view_size->Get()-m_content_size->Get()))
{
m_content_position->Assign(
m_view_size->Get()-m_content_size->Get());
}
}
}
void updateContentParentSize()
{
float estimated_size =
(m_average_delegate_size+spacing.Get())*
m_list_model->GetSize();
estimated_size -= spacing.Get();
if(m_list_delegates.empty())
{
m_content_size->Assign(estimated_size);
return;
}
auto& last_delegate = m_list_delegates.back();
float last_delegate_end =
m_get_delegate_position(last_delegate)+
m_get_delegate_size(last_delegate);
if(last_delegate->GetIndex() ==
m_list_model->GetSize()-1)
{
m_content_size->Assign(last_delegate_end);
}
else
{
m_content_size->Assign(
std::max(
last_delegate_end,
estimated_size));
}
}
void calcDelegateBounds()
{
if(layout.Get() == ListViewProperties::Layout::Column)
{
m_delegate_bounds_top =
[this](){
return -1.0f*m_content_parent->y.Get()-
delegate_extents.Get();
};
m_delegate_bounds_bottom =
[this](){
return m_delegate_bounds_top.Get()+
height.Get()+
2*delegate_extents.Get();
};
m_delegate_bounds_left =
[this](){
return m_content_parent->x.Get();
};
m_delegate_bounds_right =
[this](){
return m_delegate_bounds_left.Get()+
width.Get();
};
}
else // Row
{
throw ListViewTODO();
}
}
void calcAverageDelegateSize()
{
if(m_list_model->GetSize() == 0)
{
return;
}
m_average_delegate_size=0;
if(m_list_delegates.size() > 0)
{
for(auto& delegate : m_list_delegates)
{
m_average_delegate_size += m_get_delegate_size(delegate);
}
m_average_delegate_size /= (float)m_list_delegates.size();
}
else
{
// Instantiate some delegates
// TODO is 5 enough?
uint const delegate_count =
std::min<uint>(5,m_list_model->GetSize());
for(uint i=0; i < delegate_count; i++)
{
auto delegate =
ks::make_object<DelegateType>(
m_content_parent);
delegate->SetData(m_list_model->GetData(i));
delegate->SetIndex(i);
m_average_delegate_size += m_get_delegate_size(delegate);
m_content_parent->RemoveChild(delegate);
}
m_average_delegate_size /= (float)delegate_count;
}
// Enforce a minimum height
if(m_average_delegate_size < 1*mm)
{
m_average_delegate_size = 1*mm;
}
}
void eraseDelegatesOutsideExtents(float vext_t,
float vext_b,
float vext_l,
float vext_r)
{
uint remove_count=0;
for(auto it = m_list_delegates.end();
it != m_list_delegates.begin();)
{
--it;
auto& delegate = *it;
float delegate_t = delegate->y.Get();
float delegate_b = delegate_t + delegate->height.Get();
float delegate_l = delegate->x.Get();
float delegate_r = delegate_l + delegate->width.Get();
bool outside_vext =
(delegate_b < vext_t) ||
(delegate_t > vext_b) ||
(delegate_r < vext_l) ||
(delegate_l > vext_r);
if(outside_vext)
{
remove_count++;
m_content_parent->RemoveChild(delegate);
it = m_list_delegates.erase(it);
}
}
}
void createDebugGuidelines()
{
if(layout.Get() == ListViewProperties::Layout::Column)
{
auto& lgd = m_list_guidelines;
lgd.clear();
// Create guidelines for the delegate extents window
// top
lgd.push_back(
ks::make_object<Rectangle>(
m_content_parent,
name+"guideline_delegates_top"));
lgd.back()->width = [this](){ return width.Get(); };
lgd.back()->height = 0.25f*mm;
lgd.back()->y = [this](){ return m_delegate_bounds_top.Get(); };
// bottom
lgd.push_back(
ks::make_object<Rectangle>(
m_content_parent,
name+"guideline_delegates_bottom"));
lgd.back()->width = [this](){ return width.Get(); };
lgd.back()->height = 0.25f*mm;
lgd.back()->y = [this](){ return m_delegate_bounds_bottom.Get(); };
// Create guidelines for the list view content edges
// top
lgd.push_back(
ks::make_object<Rectangle>(
m_content_parent,
name+"guideline_content_top"));
lgd.back()->width = [this](){ return width.Get(); };
lgd.back()->height = 0.25f*mm;
lgd.back()->y = [this](){ return m_content_parent->y.Get(); };
lgd.back()->color = glm::u8vec3{255,0,255};
// bottom
lgd.push_back(
ks::make_object<Rectangle>(
m_content_parent,
name+"guideline_content_bottom"));
lgd.back()->width = [this](){ return width.Get(); };
lgd.back()->height = 0.25f*mm;
lgd.back()->y = [this](){ return m_content_parent->height.Get()-0.25f*mm; };
lgd.back()->color = glm::u8vec3{255,0,255};
log.Trace() << "height: " << m_content_parent->height.Get();
}
else
{
throw ListViewTODO();
}
}
};
// =========================================================== //
// =========================================================== //
// =========================================================== //
}
// =========================================================== //
// =========================================================== //
using namespace raintk;
int main(int argc, char* argv[])
{
(void)argc;
(void)argv;
TestContext c(480,800);
auto root = c.scene->GetRootWidget();
c.scene->SetShowDebugText(false);
auto list_view_bgbg =
ks::make_object<Rectangle>(
root,"list_view_bgbg");
list_view_bgbg->width = 60*mm;
list_view_bgbg->height = 150*mm;
list_view_bgbg->x = 0.5*(root->width.Get()-list_view_bgbg->width.Get());
list_view_bgbg->y = 0.5*(root->height.Get()-list_view_bgbg->height.Get());
list_view_bgbg->z = 0.5*mm;
list_view_bgbg->color = glm::u8vec3(30,60,60);
auto list_view_bg =
ks::make_object<Rectangle>(
root,"list_view_bg");
list_view_bg->width = 60*mm;
list_view_bg->height = 100*mm;
list_view_bg->x = 0.5*(root->width.Get()-list_view_bg->width.Get());
list_view_bg->y = 0.5*(root->height.Get()-list_view_bg->height.Get());
list_view_bg->z = 1.0;
list_view_bg->color = glm::u8vec3(60,60,60);
// ListModel
auto list_model = make_shared<ListModelSTLVector<TestItem>>();
for(uint i=0; i < 30; i++)
{
list_model->PushBack(TestItem{glm::u8vec3{96,200,90}});
}
// ListView
auto list_view =
ks::make_object<ListView<TestItem,TestDelegate>>(
list_view_bg,"list_view");
list_view->width = list_view_bg->width.Get();
list_view->height = list_view_bg->height.Get();
list_view->z = 1.5*mm;
list_view->spacing = 2.5*mm;
list_view->SetListModel(list_model);
// Run!
c.app->Run();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment