The result object of a function call is initialized by copy-initialization from the operand of the return statement. That's the same initialization that you would have e.g. for a function parameter or for initialization with = initializer syntax.
If the operand is a xvalue, such as std::move(tmp), but in a return statement also just tmp, then itcopy-initialization will result in an attempt to implicitly convert the operand to Foo, which results in an attempt toa call to the copy constructor, because implicit conversions docopy-initialization generally does not consider explicitexplicit constructors., just the same as in
Foo a;
Foo b = std::move(a);
or
void f(Foo);
Foo a;
f(std::move(a));
If however the return statement's operand is a prvalue such as Foo(std::move(tmp)), then copy-initialization means that the object will be initialized from the initializer of the prvalue. (So-called "mandatory copy elision".) The initialization of the prvalue Foo(std::move(tmp)) is direct-initialization. So the result object of the function call will be initialized by direct-initialization from the argument list (std::move(tmp)). That's the difference to earlier where it was copy-initialized from std::move(tmp).
In direct-initialization all constructors are considered against the argument list and so the explicit move constructor may be chosen. In this case std::move(tmp) is also required, because tmp is only automatically a xvalue in a return statement if it is the whole operand.
That's the same behavior as e.g. in
Foo a;
Foo b = Foo(std::move(a));
or
void f(Foo);
Foo a;
f(Foo(std::move(a)));