Expression problem

10 views
Skip to first unread message

Artyom Shalkhakov

unread,
Aug 11, 2009, 6:30:04 AM8/11/09
to fla...@googlegroups.com
Hello,

I'm trying to create a resizeable box with Flapjax. Four draggable boxes
are created, one for each edge -- top, left, right, bottom (we'll call them
"handles"). The handles define dimensions of the box.

The problem is this: every handle depends on the dimensions of the box
being resized, which, in turn, depend on the positions of other handles.
I wonder how to express this in Flapjax.

The equations I use are:

top.width = area.width
bottom.width = area.width
left.height = area.height
right.height = area.height
area.width = right.x - left.x
area.height = bottom.y - top.y

The code is on Patch-Tag:

<http://patch-tag.com/r/fjwt/snapshot/current/content/pretty/dnd>
(dragdrop.js, lines 358-394)

Cheers,
Artyom Shalkhakov.

Arjun Guha

unread,
Aug 11, 2009, 10:23:20 AM8/11/09
to fla...@googlegroups.com
I think there are two things that make this hard. First, you need to
break the cycle in those constraints using delayB or delayE.

Once you're past that, the obvious approach is to define a behavior
(e.g. top.width) that is dependent on the time-varying value of the
DOM attribute dom.width. You can do this with the DOMAttrModified
event, which isn't well supported. You could try using the following
function I wrote. It uses Prototype and Flapjax. It does not work on
Safari 4, but it does work on Firefox 3.5:

// posB :: Element -> Behavior { left, top, width, height: String }
function posB(t_) {
var t = $(t_); // adds Prototype methods
var f = function() {
var pos = t.cumulativeOffset();
var size = t.getDimensions();
return { left : pos.left, top: pos.top,
height: size.height, width: size.width }};

return $E(t, "DOMAttrModified")
.filterE(function(x) { return x.attrName == "style"; })
.mapE(f)
.startsWith(f());
}

The alternative approach is to build a data structure with al the
variables you're interested in, setup the equations and project back
and forth from the DOM (a.k.a. "lenses"). I'll give that a shot this
evening. Let me know if DOMAttrModified works for you.

Arjun

Artyom Shalkhakov

unread,
Aug 12, 2009, 2:19:18 AM8/12/09
to fla...@googlegroups.com
Hello Arjun,

2009/8/11 Arjun Guha <arjun...@gmail.com>:


> I think there are two things that make this hard.  First, you need to
> break the cycle in those constraints using delayB or delayE.

I don't understand this. Can you show where the cycle is, specifically?

> Once you're past that, the obvious approach is to define a behavior
> (e.g. top.width) that is dependent on the time-varying value of the
> DOM attribute dom.width.  You can do this with the DOMAttrModified
> event, which isn't well supported.  You could try using the following
> function I wrote.  It uses Prototype and Flapjax.  It does not work on
> Safari 4, but it does work on Firefox 3.5:

Well, this suggests selector functions for DOM objects managed by
Flapjax. Is this possible with current implementation?

> // posB :: Element -> Behavior { left, top, width, height: String }
> function posB(t_) {
>   var t = $(t_); // adds Prototype methods
>   var f = function() {
>     var pos = t.cumulativeOffset();
>     var size = t.getDimensions();
>     return { left : pos.left, top: pos.top,
>              height: size.height, width: size.width }};
>
>   return $E(t, "DOMAttrModified")
>     .filterE(function(x) { return x.attrName == "style"; })
>     .mapE(f)
>     .startsWith(f());
> }

I don't quite like this approach, because it doesn't work on all
browsers (and it feels like a hack, I must admit).

> The alternative approach is to build a data structure with al the
> variables you're interested in, setup the equations and project back
> and forth from the DOM (a.k.a. "lenses").  I'll give that a shot this
> evening.  Let me know if DOMAttrModified works for you.

This is interesting. I think it would be necessary for GUI layout as well
(element layout has mixed top-down bottom-up dependencies -- you
can't express that using current Flapjax without resorting to hacks AFAIK).

Cheers,
Artyom Shalkhakov.

Arjun Guha

unread,
Aug 12, 2009, 7:03:24 AM8/12/09
to fla...@googlegroups.com
> I don't understand this. Can you show where the cycle is,
> specifically?

I think I misunderstood the system you want to setup. Let me try to
restate it:

You have a central area (that cannot be dragged) and four edges that
can be dragged, called top, right, bottom, left. As you drag and top
and left edges, shouldn't the position of the area move? I'd expect to
see area.left and area.right somewhere in those equations.

So, focusing just on the top edge and area, I'd have imagined you'd
want:

area.top = top.top + top.height
top.top = ... determined by dragging ...
top.height = ... constant ...

Let me know what you think.

As for the cycles... I may be wrong about that. I believe I was
imaging dragging corners.

>
> Well, this suggests selector functions for DOM objects managed by
> Flapjax. Is this possible with current implementation?

The only two functions used from Prototype in that code are
cumulativeOffset and getDimensions. cumulativeOffset is in Flapjax; I
lifted it from Prototype. We could add getDimensions too, but at some
point it's just easier to use jQuery/Prototype instead of replicating
their functionality in Flapjax.

Arjun

Artyom Shalkhakov

unread,
Aug 12, 2009, 7:17:17 AM8/12/09
to fla...@googlegroups.com
2009/8/12 Arjun Guha <arjun...@gmail.com>:

>
>> I don't understand this. Can you show where the cycle is,
>> specifically?
>
> I think I misunderstood the system you want to setup.  Let me try to
> restate it:
>
> You have a central area (that cannot be dragged) and four edges that
> can be dragged, called top, right, bottom, left.

Right.

>  As you drag and top and left edges, shouldn't the position of the area
> move? I'd expect to see area.left and area.right somewhere in those
> equations.

Yes it should! I haven't included those because I thought it would be
"trivial" to get that detail right after edges' width and height are
dealt with. Sorry about that.

> So, focusing just on the top edge and area, I'd have imagined you'd
> want:
>
> area.top = top.top + top.height
> top.top = ... determined by dragging ...
> top.height = ... constant ...

Precisely! top.height might be a behavior but it shouldn't really
matter.

> As for the cycles... I may be wrong about that.  I believe I was
> imaging dragging corners.

Imagine constraining central area's width and height so that it is
within certain bounds, or even bounding draggable edges to an
outer element's dimensions. Would they introduce cycles?

Cheers,
Artyom Shalkhakov.

Arjun Guha

unread,
Aug 14, 2009, 10:53:58 AM8/14/09
to Flapjax
How about this solution. You can drag horizontal edges up/down and
vertical edges left/right. The trick for me was to realize that it
doesn't make must sense to drag a horizontal *edge* horizontally.
(Corners are a different matter.) The corners here are poorly
overlapped, but it's just a matter of constant offsets.

Arjun

<html>

<head>
<title>Flapjax Demo: Resizable Box</title>

<style type="text/css">

.handle {
background-color: #000000;
border: 1px solid #666666;
color: #ffffff;
position: absolute;
font-family: sans-serif;
cursor: move;
width: 10px;
height: 10px;
}

.horzEdge {
background-color: #eeeeee;
border: 1px solid #666666;
color: #ffffff;
position: absolute;
font-family: sans-serif;
cursor: move;
width: 200px;
top: 200px;
height: 10px;
}

.vertEdge {
background-color: #eeeeee;
border: 1px solid #666666;
color: #ffffff;
position: absolute;
font-family: sans-serif;
cursor: move;
left: 200px;
width: 10px;
height: 200px;
}




</style>

<script type="text/javascript" src="../fx/flapjax-impl.js"></script>
<script type="text/javascript" src="../fx/prototype.js"></script>
<script type="text/javascript">

// dragE(element) -> EventStream { left: int, top: int }
// From the Flapjax OOPSLA paper.
function dragE(target) {
// moveEe :: EventStream (EventStream { left: int, top: int })
var moveEe = extractEventE(target,'mousedown').mapE(function(md) {
var startX = md.layerX;
var startY = md.layerY;

return extractEventE(document,'mousemove').mapE(function(mm) {
// Stop the text from getting selected
mm.preventDefault();

return { element: target, // convenience
left: mm.clientX - startX,
top: mm.clientY - startY };
});
});

// dropEe :: EventStream (EventStream 'a)
var dropEe = extractEventE(document,'mouseup').mapE(function() {
return zeroE();
});

return switchE(mergeE(moveEe,dropEe));
}

function plusB(xB, yB) {
return liftB(function(x, y) { return x + y; }, xB, yB);
}

function minusB(xB, yB) {
return liftB(function(x, y) { return x - y; }, xB, yB);
}

function loader() {

var dragLeft = dragE($("leftEdge")) .startsWith({ left: 20, top:
10 });
var dragBottom = dragE($("bottomEdge")).startsWith({ left: 10, top:
200 });
var dragRight = dragE($("rightEdge")) .startsWith({ left: 210, top:
10 });
var dragTop = dragE($("topEdge")).startsWith({ left : 10, top:
20 });

var topLeft = dragTop.liftB(function(p) { return p.left; });
var topTop = dragTop.liftB(function(p) { return p.top } );
var bottomLeft = dragBottom.liftB(function(p) { return p.left; });
var bottomTop = dragBottom.liftB(function(p) { return p.top } );
var rightLeft = dragRight.liftB(function(p) { return p.left; });
var rightTop = dragRight.liftB(function(p) { return p.top; });
var leftLeft = dragLeft.liftB(function(p) { return p.left; });
var leftTop = dragLeft.liftB(function(p) { return p.top; });

insertValueB(plusB(topTop, 10), "area", "style", "top");
insertValueB(plusB(leftLeft, 10), "area", "style", "left");
insertValueB(minusB(minusB(rightLeft, leftLeft), 10),
"area", "style", "width");
insertValueB(minusB(minusB(bottomTop, topTop), 10),
"area", "style", "height");

insertValueB(leftLeft, $("leftEdge"), "style", "left");
insertValueB(topTop, $("leftEdge"), "style", "top");
insertValueB(minusB(bottomTop, topTop), $("leftEdge"), "style",
"height");
insertValueB(constantB(10), $("leftEdge"), "style", "width");

insertValueB(topTop, $("topEdge"), "style", "top");
insertValueB(plusB(leftLeft, 10), $("topEdge"), "style", "left");
insertValueB(minusB(rightLeft, leftLeft), $("topEdge"), "style",
"width");
insertValueB(constantB(10), $("topEdge"), "style", "height");

insertValueB(bottomTop, $("bottomEdge"), "style", "top");
insertValueB(plusB(leftLeft, 10), $("bottomEdge"), "style", "left");
insertValueB(minusB(rightLeft, leftLeft), $("bottomEdge"), "style",
"width");
insertValueB(constantB(10), $("bottomEdge"), "style", "height");

insertValueB(topTop, $("rightEdge"), "style", "top");
insertValueB(rightLeft, $("rightEdge"), "style", "left");
insertValueB(constantB(10), $("rightEdge"), "style", "width");
insertValueB(minusB(bottomTop, topTop),
$("rightEdge"), "style", "height");



}

</script>
<link rel="stylesheet" href="/demo.css"/>
</head>

<body onload="loader()">

<div id="area" style="left: 30px; top: 30px; position: absolute;
background-color: green">
</div>
<div id="topEdge" class="horzEdge"></div>
<div id="bottomEdge" class="horzEdge"></div>
<div id="rightEdge" class="vertEdge"></div>
<div id="leftEdge" class="vertEdge"></div>

</body>

</html>

Arjun Guha

unread,
Aug 14, 2009, 11:00:52 AM8/14/09
to fla...@googlegroups.com
I ought to point out that it doesn't need prototype, even though it's
included.

Arjun

Artyom Shalkhakov

unread,
Aug 17, 2009, 5:35:12 AM8/17/09
to fla...@googlegroups.com
2009/8/14 Arjun Guha <arjun...@gmail.com>:

>
> How about this solution.  You can drag horizontal edges up/down and
> vertical edges left/right.  The trick for me was to realize that it
> doesn't make must sense to drag a horizontal *edge* horizontally.
> (Corners are a different matter.)  The corners here are poorly
> overlapped, but it's just a matter of constant offsets.

A good one, much much better than sources of the image croppers I've
checked out. And very easy, just convert your constraints to code. :)

Thanks a lot!

However, there are other questions remaining open:
- how to make this functionality reusable?
- what about dynamically created markup? (via JS)
- is there a way to assign constraints once?

but I guess these are less important.

Cheers,
Artyom Shalkhakov.

Arjun Guha

unread,
Aug 17, 2009, 6:48:11 AM8/17/09
to fla...@googlegroups.com
> A good one, much much better than sources of the image croppers I've
> checked out. And very easy, just convert your constraints to code. :)

So, that's what this is about! Let us in on the final result, if you
can.

> - what about dynamically created markup? (via JS)
> - how to make this functionality reusable?

I think these two are related and fairly easy, depending on your
definition of reusable. You can generate markup.

> - is there a way to assign constraints once?

Unfortunately, that code does have to setup 20 constraints. If you
worked with dynamically created markup, and used Flapjax's HTML
constructors, you could simultaneously specify the dimensions of each
edge:

DIV({ style: { left: ... as before ...,
top: ... as before ...,
width: ... as before ..,
height: ... as before ...,
... inline attributes from horzEdge ... });

You'd still write 20 constraints, but it would be slightly cleaner.

I don't think there is any way to avoid writing each constraint
explicitly, which is why an abstraction would help.

Arjun

Artyom Shalkhakov

unread,
Aug 19, 2009, 5:41:50 AM8/19/09
to fla...@googlegroups.com
2009/8/17 Arjun Guha <arjun...@gmail.com>:

>> A good one, much much better than sources of the image croppers I've
>> checked out. And very easy, just convert your constraints to code. :)
>
> So, that's what this is about!  Let us in on the final result, if you
> can.

Will do, of course! I'm working on a widget toolkit, actually, and
intend to make it
freely available. The motivation is to make it easier to program and extend
in-browser applications.

Yes I know about Dojo/Dijit, YUI, Qooxdoo, jQuery UI -- but they are
IMHO hard to
work with, as they lack compositionality almost by design (and they have lots of
policies (but no mechanisms), -- that's where most of them differ!). I tried to
integrate Qooxdoo with Flapjax but then I didn't like how it was turning out, so
I decided to go another way.

>> - what about dynamically created markup? (via JS)
>> - how to make this functionality reusable?
>
> I think these two are related and fairly easy, depending on your
> definition of reusable.  You can generate markup.

What I meant was that
- resizeable elements are quite widespread:
image cropping, dialog/window resizing, selection (not quite
cropping!), panels, etc.
- sometimes you don't want to resize a particular edge (or two or three),
e.g. Windows doesn't allow you to resize left/top window edges while NeXT
allowed dragging only the bottom edge and corners
- sometimes resizing affects other elements:
imagine resizing two panels laid out from left to right
"stitched" by a vertical element (the one you'd move to resize them)
- sometimes you'd like to limit minimum and maximum size,
or constrain width to height ratio or even snap to grid

There are lots of use-cases to consider. We can surely go the easy route,
making the API complex trying to cover all our use-cases, but I'd like to
define widgets via combinators. Mechanism not policy, like Flapjax.

>> - is there a way to assign constraints once?
>
> Unfortunately, that code does have to setup 20 constraints.  If you
> worked with dynamically created markup, and used Flapjax's HTML
> constructors, you could simultaneously specify the dimensions of each
> edge:
>
> DIV({ style: { left: ... as before ...,
>                top: ... as before ...,
>                width: ... as before ..,
>                height: ... as before ...,
>                ... inline attributes from horzEdge ... });
>
> You'd still write 20 constraints, but it would be slightly cleaner.

Oops, I must have been vague. I meant that the code you posted
destructively updates left/width/top/height constraints for each edge.
Is there a way to do single assignment here?

> I don't think there is any way to avoid writing each constraint
> explicitly, which is why an abstraction would help.

This is great and such, but I don't have an idea how would it look like.

I'm experimenting with directed interval algebra at the moment, it
seems useful:
- a rectangle is modeled by two closed intervals (one for each axis)
but "improper" intervals are allowed
- width and height of resizeable area should be just an operation away
- wrapping is expressible by direction

Cheers,
Artyom Shalkhakov.

Artyom Shalkhakov

unread,
Aug 20, 2009, 6:07:34 AM8/20/09
to fla...@googlegroups.com
2009/8/19 Artyom Shalkhakov <artyom.s...@gmail.com>:

> I'm experimenting with directed interval algebra at the moment, it
> seems useful:
> - a rectangle is modeled by two closed intervals (one for each axis)
> but "improper" intervals are allowed
> - width and height of resizeable area should be just an operation away
> - wrapping is expressible by direction

So I did it using the directed interval algebra. Seems easier this way.

Here's how the code looks like:

function loader() {
// commutative version of betweenD
function between(a,b) {
return lleD(a,b)? betweenD(a,b) : betweenD(b,a);
}
// set constraints up
var leftBD = dragBD('leftEdge','left',directed(20,30));
var rightBD = dragBD('rightEdge','left',directed(210,220));
var topBD = dragBD('topEdge','top',directed(20,30));
var bottomBD = dragBD('bottomEdge','top',directed(200,210));
var widthBD = liftB(between,leftBD,rightBD);
var heightBD = liftB(between,topBD,bottomBD);
// assign them to elements
// insertBR :: (behavior rect) (string | dom) -> unit
function insertBR(x,dom) {
insertValueB({left:liftB(leftR,x),top:liftB(topR,x)
,width:liftB(widthR,x),height:liftB(heightR,x)},
dom, 'style');
}
insertBR(liftB(directedR,widthBD,heightBD),'area');
insertBR(liftB(directedR,leftBD,heightBD),'leftEdge');
insertBR(liftB(directedR,rightBD,heightBD),'rightEdge');
insertBR(liftB(directedR,widthBD,topBD),'topEdge');
insertBR(liftB(directedR,widthBD,bottomBD),'bottomEdge');
}

(the rest is here:
<http://patch-tag.com/r/fjwt/snapshot/current/content/pretty/dnd/resize.html>)

A rectangle is represented by a pair of intervals, ranging over X or Y
axis, respectively. "Improper" intervals are allowed (an interval is
improper if its left bound is greater than its right bound), this accounts
for wrapping.

We have a special "between" operation, which, given two intervals,
yields the shortest distance between them, represented by an interval.
This is how we obtain width and height.

Now, how to make this dynamic? Arjun, do you have any idea?

Cheers,
Artyom Shalkhakov.

Reply all
Reply to author
Forward
0 new messages