Skip to content

Instantly share code, notes, and snippets.

@clayote
Last active December 22, 2015 19:29
Show Gist options
  • Save clayote/b32ae6256d91fa7d492a to your computer and use it in GitHub Desktop.
Save clayote/b32ae6256d91fa7d492a to your computer and use it in GitHub Desktop.
Test for deck builder layout
# This is meant to be a deck builder for trading card games like Magic the Gathering.
# You drag cards from your collection on the right to your deck on the left.
# The order may be significant.
# A gap will open where your card will drop, if you release it.
# When you drop it, it will snap into place.
#
# Problem:
# If you drag a card from one stack to the front of the other, and then press Reset,
# the card goes back to its original position like it should, but it leaves behind
# a non-interactive afterimage.
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.logger import Logger
from kivy.properties import (
AliasProperty,
BooleanProperty,
DictProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
ReferenceListProperty,
StringProperty,
BoundedNumericProperty
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.layout import Layout
from kivy.uix.stencilview import StencilView
from kivy.uix.widget import Widget
"""Widget that looks like a trading card, and a layout within which it
can be dragged and dropped to some particular position within stacks
of other cards.
"""
dbg = Logger.debug
class Card(FloatLayout):
"""A trading card, similar to the kind you use to play games like
_Magic: the Gathering_.
"""
deck = NumericProperty()
idx = NumericProperty()
collide_x = NumericProperty()
collide_y = NumericProperty()
collide_pos = ReferenceListProperty(collide_x, collide_y)
foreground_color = ListProperty([1, 1, 1, 1])
text = StringProperty('')
text_color = ListProperty([0, 0, 0, 1])
markup = BooleanProperty(True)
font_name = StringProperty('Roboto-Regular')
font_size = NumericProperty(12)
class DeckBuilderLayout(Layout):
"""Sizes and positions :class:`Card` objects based on their order
within ``decks``, a list of lists where each sublist is a deck of
cards.
"""
decks = ListProperty([[]]) # list of lists of cards
"""Put a list of lists of :class:`Card` objects here and I'll position
them appropriately. Please don't use ``add_widget``.
"""
insertion_deck = BoundedNumericProperty(None, min=0, allownone=True)
"""Index of the deck that a card is being dragged into."""
insertion_card = BoundedNumericProperty(None, min=0, allownone=True)
"""Index within the current deck that a card is being dragged into."""
dragging = ObjectProperty(None, allownone=True)
def __init__(self, **kwargs):
"""Bind most of my custom properties to ``_trigger_layout``."""
super(self.__class__, self).__init__(**kwargs)
self.bind(
decks=self._trigger_layout,
insertion_deck=self._trigger_layout,
insertion_card=self._trigger_layout
)
def on_decks(self, *args):
"""Inform the cards of their deck and their index within the deck;
extend the ``_hint_offsets`` properties as needed; and trigger
a layout.
"""
if None in (
self.canvas,
self.decks,
):
Clock.schedule_once(self.on_decks, 0)
return
decknum = 0
for deck in self.decks:
cardnum = 0
for card in deck:
if not isinstance(card, Card):
raise TypeError("You must only put Card in decks")
if card not in self.children:
self.add_widget(card)
if card.deck != decknum:
card.deck = decknum
if card.idx != cardnum:
card.idx = cardnum
cardnum += 1
decknum += 1
self._trigger_layout()
def on_touch_down(self, touch):
"""If I'm the first card to collide this touch, grab it, store my
metadata in its userdict, and store the relative coords upon
me where the collision happened.
"""
if not self.collide_point(*touch.pos):
return
for child in self.children:
if not isinstance(child, Card):
continue
if child.collide_point(*touch.pos) and (
self.dragging is None or (
child.idx > self.dragging.idx or
child.deck > self.dragging.deck
)
):
self.dragging = child
if self.dragging is not None:
self.dragging.collide_x = touch.x - self.dragging.x
self.dragging.collide_y = touch.y - self.dragging.y
touch.grab(self)
def on_touch_move(self, touch):
"""If a card is being dragged, move other cards out of the way to show
where the dragged card will go if you drop it.
"""
if touch.grab_current != self:
return
self.dragging.pos = (
touch.x - self.dragging.collide_x,
touch.y - self.dragging.collide_y
)
if not hasattr(self.dragging, '_topdecked'):
self.canvas.after.add(self.dragging.canvas)
self.dragging._topdecked = True
i = 0
for deck in self.decks:
cards = [card for card in deck if card != self.dragging]
maxidx = max(card.idx for card in cards) if cards else 0
cards.reverse()
cards_collided = [
card for card in cards if card.collide_point(*touch.pos)
]
if cards_collided:
collided = cards_collided.pop()
for card in cards_collided:
if card.idx > collided.idx:
collided = card
if self.dragging and collided.deck == self.dragging.deck:
self.insertion_card = (
1 if collided.idx == 0 else
maxidx + 1 if collided.idx == maxidx else
collided.idx + 1 if collided.idx > self.dragging.idx
else collided.idx
)
else:
dropdeck = self.decks[collided.deck]
maxidx = max(card.idx for card in dropdeck)
self.insertion_card = (
1 if collided.idx == 0 else
maxidx + 1 if collided.idx == maxidx else
collided.idx + 1
)
if self.insertion_deck != collided.deck:
self.insertion_deck = collided.deck
return
else:
if self.insertion_deck == i:
if self.insertion_card in (0, len(deck)):
pass
elif touch.y > cards[0].top or touch.x < cards[0].x:
self.insertion_card = len(deck)
elif touch.y < cards[0].y or touch.x > cards[0].right:
self.insertion_card = 0
i += 1
def on_touch_up(self, touch):
"""If a card is being dragged, put it in the place it was just dropped
and trigger a layout.
"""
if touch.grab_current != self:
return
touch.ungrab(self)
if hasattr(self.dragging, '_topdecked'):
self.canvas.after.remove(self.dragging.canvas)
del self.dragging._topdecked
if None not in (self.insertion_deck, self.insertion_card):
# need to sync to adapter.data??
del self.decks[self.dragging.deck][self.dragging.idx]
for i in range(0, len(self.decks[self.dragging.deck])):
self.decks[self.dragging.deck][i].idx = i
deck = self.decks[self.insertion_deck]
if self.insertion_card >= len(deck):
deck.append(self.dragging)
else:
deck.insert(self.insertion_card, self.dragging)
self.dragging.deck = self.insertion_deck
self.dragging.idx = self.insertion_card
self.decks[self.insertion_deck] = deck
self.insertion_deck = self.insertion_card = self.dragging = None
self._trigger_layout()
def on_insertion_card(self, *args):
"""Trigger a layout"""
if self.insertion_card is not None:
self._trigger_layout()
def do_layout(self, *args):
"""Layout each of my decks"""
if self.size == [1, 1]:
return
self.clear_widgets()
for i in range(0, len(self.decks)):
self.layout_deck(i)
def layout_deck(self, i):
"""Stack the cards, starting at my deck's foundation, and proceeding
by ``card_pos_hint``
"""
def get_dragidx(cards):
j = 0
for card in cards:
if card == self.dragging:
return j
j += 1
# Put a None in the card list in place of the card you're
# hovering over, if you're dragging another card. This will
# result in an empty space where the card will go if you drop
# it now.
cards = list(self.decks[i])
dragidx = get_dragidx(cards)
if self.insertion_deck == i and self.insertion_card is not None:
insdx = self.insertion_card
if dragidx is not None and insdx > dragidx:
insdx -= 1
cards.insert(insdx, None)
cards.reverse()
# Work out the initial pos_hint for this deck
phx = 0.1
phy = 0.6
phx += i * 0.4
(w, h) = self.size
(x, y) = self.pos
# start assigning pos and size to cards
for card in cards:
if card is not None:
(shw, shh) = (0.15, 0.3)
if card != self.dragging:
card.pos = (
x + phx * w,
y + phy * h
)
card.size = (w * shw, h * shh)
self.add_widget(card)
phx += 0.05
phy -= 0.1
class DeckBuilderView(DeckBuilderLayout, StencilView):
"""Just a :class:`DeckBuilderLayout` mixed with
:class:`StencilView`.
"""
pass
kv = """
<Card>:
canvas:
Color:
rgba: [1, 1, 1, 1]
Rectangle:
pos: root.pos
size: root.size
BoxLayout:
size_hint: 0.9, 0.9
pos_hint: {'x': 0.05, 'y': 0.05}
orientation: 'vertical'
Widget:
id: foreground
canvas:
Color:
rgba: root.foreground_color
Rectangle:
size: self.size
pos: self.pos
Label:
text: root.text
color: root.text_color
markup: root.markup
font_name: root.font_name
font_size: root.font_size
text_size: foreground.size
size_hint: (None, None)
size: self.texture_size
pos: foreground.pos
valign: 'top'
<DeckBuilderScrollBar>:
ScrollBarBar:
id: bar
color: root.bar_color if root.scrolling else root.bar_inactive_color
texture: root.bar_texture
"""
Builder.load_string(kv)
if __name__ == '__main__':
from kivy.uix.button import Button
def decko():
return [
Card(
foreground_color=[0, 0, 1, 1],
text='The quick brown fox jumps over the lazy dog',
text_color=[1, 1, 1, 1],
)
for i in range(0, 4)
]
deck0 = decko()
def deckun():
return [
Card(
foreground_color=[1, 0, 0, 1],
text='Have a steak at the porter house bar',
text_color=[1, 1, 0, 1],
)
for i in range(0, 4)
]
deck1 = deckun()
from kivy.base import runTouchApp
from kivy.core.window import Window
from kivy.modules import inspector
builder = DeckBuilderLayout(
pos_hint={'x': 0, 'y': 0},
decks=[deck0, deck1]
)
layout = BoxLayout(orientation='vertical')
layout.add_widget(builder)
def redeux(*args):
builder.decks = [decko(), deckun()]
layout.add_widget(Button(
text='Reset',
size_hint_y=0.1,
on_release=redeux
))
inspector.create_inspector(Window, layout)
runTouchApp(layout)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment